アクセシビリティを意識したアコーディオンを作ってWAI-ARIAを学んでみたお話
この記事は 「Webアクセシビリティ Advent Calendar 2020」 10日目の記事です。(執筆が終わったのが11日目になってしまい申し訳ございません)
先日投稿したWebアクセシビリティ Advent Calendarの記事「キーボード操作を意識したHTML/CSSコーディング」は思いがけず大変な反響をいただきました。ありがとうございます。
今回はWeb制作において頻出度の高いアコーディオンをアクセシビリティを意識しながら作ってみたという「やってみた」系記事です。
今回の記事を書くにあたり、そめさんよりいくつかレビューを頂きました(ありがとうございます)。今回はそのレビューも含めて修正前と修正後も同時掲載します。
はじめに
はじめに今回のアコーディオンのサンプルを掲載します。(サンプルではaria
属性の付与はJSで行っています)
この記事を書いた理由は
- WAI-ARIAの知識が少し不安だったので備忘録を兼ねて再学習記録を書こうと思った
- こういった学習記録を書いておくと有識者からコメントをいただけるかもしれない
といったところです。
現代のHTMLではdetails
要素とsummary
要素を合わせて使えば簡単に開閉ができる折りたたみ式のウィジェットを作ることができますが、僕が受け持つ案件は大抵「IE対応」と「パネル開閉時のアニメーション」どちらも要求されがちで利用機会が無いので自作する必要があるんですよね…。
<details>
<summary>見出し</summary>
このテキストが折りたたまれます。こんな感じで。
</details>
今回のテーマはアコーディオンを作りながらWAI-ARIAを学ぶなのでdetails
要素とsummary
要素については今回はノータッチでいきます。今までは昔書いた汚いアコーディオンのテンプレートを使いまわしていましたが、これを機にアップデートしていこうという理由もあります。
WAI-ARIAとは?
まず、WAI-ARIAとはなんぞや?から。
WAI-ARIAは「Web Accessibility Initiative - Accessible Rich Internet Applications」
の頭文字を取ったもので、HTMLやSVGに付与して利用できるアクセシビリティ確保のための属性の仕様です。
WAI
W3C内のWeb Accessibility Initiative(WAI)
という団体・組織の名称で、標準のブラウザだけでなく様々なユーザーエージェントを用いてWebを利用する方々のためにウェブアクセシビリティの向上を目的としている団体です。
『Web Content Accessibility Guidelines』というウェブアクセシビリティに関する指針はWAIによって公開されています。
ARIA
WAIによって公開されているウェブアクセシビリティに関する仕様書です。
ウェブアクセシビリティへの考え方からアクセシビリティを満たすコーディング方法まで記載されています。
■参考文献:Accessible Rich Internet Applications (WAI-ARIA) 1.1 日本語訳
WAI-ARIAの2種類の属性
WAI-ARIAではrole属性
とaria属性
という2種類の属性が定義されています。
role属性
簡単に言えばこれは見出しなのか?ボタンなのか?ナビゲーションなのか?などのコンテンツの役割を表すための属性です。
<div role="button">サンプル</div>
div
は原則的に何も意味を持たないタグですが、role="button"
を付与することでそのdiv
はボタンとしての役割を持つようになり、スクリーンリーダー(音声ブラウザ)などの支援技術はそれを「ボタン」と認識することができます。
僕たちはWebサイトのコンテンツを見ただけで「これはタブだな」とか「これは見出しだな」と判断することができますが、音声でインターネットしている方々には理解が難しい場合もあります。そういった方々に「これは◯◯です」と伝えるための属性だと覚えておきます。
ただし、HTMLタグには「暗黙のARIAセマンティック」が適用されているものがあります。例えばh1
〜h6
にはrole="heading"
が既に適用されていますし、button
にはrole="button"
が既に適応されています。それらを重複して指定することは無意味です。(ただし、main
などレガシーブラウザが対応していないようなタグはレガシーブラウザでは役割を認識できないので重複してrole="main"
を付与させるケースもあります)
<!-- 重複してつける意味がない -->
<h2 role="heading"></h2>
<nav role="navigation"></nav>
<!-- IE11でも認識させたい場合は重複しても仕方がない -->
<main role="main"></main>
あくまでもHTMLタグを正しく使ってセマンティックにマークアップした上で他に役割を知らせたいことがあったらrole属性
を使いましょう。HTMLをどうマークアップするか?は「スクリーンリーダーでどう読み上げられたいか?」を意識すれば迷うことはないような気がします。
<!-- SVG要素を画像として認識させたい -->
<svg role="img"></svg>
<!-- a要素をタブと認識させたい -->
<a role="tab" href="#"></main>
<!-- バリデーションなどで警告として認識させたい -->
<p role="alert">電話番号は必須です</p>
また、暗黙のARIAセマンティックを上書きする際は気をつけましょう。重要な役割を失ってしまう可能性があります。
<!-- 見出しとしての意味が消えてしまうのでよくない -->
<h3 role="button" tabindex="1"></h3>
aria属性
表示されているor隠れているといったコンテンツの状態明示や、どの箇所から参照されているか?などの特性明示を支援技術ユーザーに対して行うことができます。
classの付け替えで状態管理を行っているアコーディオンは目で見てそれが開いているかor閉じているかを判断することは容易ですが、支援技術はそれがどうなっているかを知ることができず、支援技術ユーザーに「アコーディオンが現在開いているか閉じているか」といった状態を伝えることはできません。
そこでaria属性で状態を明示すれば支援技術ユーザーにどうなっているかを伝えることができるようになります。
サンプルのアコーディオンをVoiceOverで読み上げた例ですが、しっかりと「折り畳まれました」と伝えられていますよね?
また、読み上げに含まれている「タブを開閉する」は実際には実際には表示されていないテキストです。これはaria-label
という特性明示のaria属性を使い、支援技術ユーザーに対してのみ読み上げを行っています。
<span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>
このようにWAI-ARIAを用いてコンテンツ内の役割や特性、状態を明示することで、支援技術はWebページ内の構成や文脈、動きなどを理解することが可能になり、支援技術を利用しているユーザーに的確な情報を与えることができるようになります。
また、CSS設計においてもDOMの状態管理を名前のバラつきを起こしにくく、セマンティックに行うことができるようになるのもWAI-ARIAを用いることのメリットの一つですね。
/**
* こう書いても悪くはないけれど
*/
.burger-button.is-active {
/* ハンバーガーメニューが開いているときのハンバーガーボタンのスタイル */
}
/**
* aria属性で指定したほうが名前のバラつきが起こりにくく標準化された属性なのでよりセマンティックに
*/
.burger-button[aria-expanded="true"] {
/* ハンバーガーメニューが開いているときのハンバーガーボタンのスタイル */
}
■参考文献:
WAI-ARIA 実装の5つのルール | Accessible & Usable
WAI-ARIAを活用したフロントエンド実装 | 第1回 role属性、aria属性の基礎知識 | CodeGrid
アコーディオンの骨組み
まず、HTMLでアコーディオンの骨組みを作ってみます。僕は以下のような形で組んでみました。
<div class="my-accordion js-accordion">
<section class="my-accordion__item">
<h2 class="my-accordion__headline">
<button type="button" class="my-accordion__tab js-accordion-tab">
見出し
<span class="my-accordion__tabicon"></span>
</button>
</h2>
<div class="my-accordion__panel js-accordion-panel">
<div class="my-accordion__panel-content">
文章
</div>
</div>
</section>
...
</div>
解説
- 見出し+テキストのセクション構造にしています。これによってスクリーンリーダーの「見出しジャンプ機能」 を利用することができるようになり、支援技術を利用しているユーザーは見出しを拾い読みしてWebページのアウトラインをざっと把握することができるようになります。
- スクリーンリーダーやキーボードのTab操作で選択ができるようにアコーディオンを開閉するタブは必ず
a
かbutton
で実装しましょう。divで実装したり、見出しそのものにクリックイベントを付けるのはアクセシブルではありません。(理由は前回の記事を参照) - jQueryの
slideToggle
的なアニメーションを用いたい場合はパネルのdivの中にコンテナのdivを追加しておいたほうがいいです。コンテナが無いと高さの再取得が辛くなってきます(僕がJS弱者なだけかもしれない)。slideToggle
のためだけにjQuery使いたくなるのは僕だけでしょうか…。 - スタイルを指定するためのclassとJSで参照するclassは分けています。今回の場合だと気にならないかもしれませんが、CSSは適用したいがJSの定義は不要、またはその逆ってパターンが起こった際に困ります。
アコーディオンを開閉するタブにWAI-ARIAを導入してみる
骨組みが終わったので早速WAI-ARIAを用いてアコーディオンのパーツに特性や状態を明示してみました。僕は以下のような形で属性を付与してみましたがいかがでしょうか?
<h2 class="my-accordion__headline">
<button type="button"
id="accordion-tab-number-1"
class="my-accordion__tab js-accordion-tab"
aria-expanded="false"
aria-controls="accordion-panel-number-1">
見出し
<span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>
</button>
</h2>
解説
- タブに連番を振った
id
を付与する。アコーディオンのパネルに付与するaria-labelledby
属性と紐付けを行うために必要です。 - アコーディオンが閉じている(false)か開いている(true)かを伝えるためにタブに
aria-expanded
属性を付与します。タブの状態管理に用いたいので今回はタブに付与させることにしました。 - パネルがどの要素から操作されるかを指定するためにタブに
aria-controls
属性を付与します。操作をされるパネルにaria-controls
の属性値と同じid
を指定することで関連付けをします。 - 「ボタン」と読ませるために
button
タグを利用。暗黙のARIAセマンティクスを尊重します。role属性
は付与しません。アコーディオンの"タブ"だからrole="tab"
を付与したら?と思うけどこれに関しては後述。汎用的なボタンなのでtype="button"
を付与します。 - タブ内のアイコン(
.my-accordion__tabicon
)に意味を持たせるためにaria-label
属性を指定してその中に代替テキストを含めます。
注意点
-
aria-expanded
属性は開閉している要素自身につけるのではなく、それを制御している要素もしくは開閉している要素の親要素につけること。今回の例だと開閉しているパネル(div.my-accordion__panel
)に付与するのではなく、制御しているタブ(button.my-accordion__tab
)もしくはパネルの親要素(section.my-accordion__item
)に付与します。
■参考記事:aria-expandedのよくある間違い / masuP.net
レビューを受けて
1. aria-label="タブを開閉する"
はいらないかも
このボタンが開閉できること自体と、開閉の状態はどちらもaria-expanded
属性によってすでに担保されているため、aria-label="タブを開閉する"
は冗長になってしまうとのことです。
ハンバーガーメニューのアイコンにaria-label="メニューを開閉する"
と書くのは冗長なので「メニュー」でOKという理由と同じだそう。
<button type="button"
id="js-menu-toggle"
class="menu-toggle"
aria-controls="global-nav"
aria-expanded="false"
aria-label="メニュー">
<span class="menu-toggle__line"></span>
<span class="menu-toggle__line"></span>
<span class="menu-toggle__line"></span>
</button>
ハンバーガーアイコンのマークアップはこんな感じでOKだそうです。
2. 暗黙的なrole
のない要素にaria-label
属性をつけるのは非推奨
明示的ないし暗黙的なrole
が存在しない要素にaria-label
属性を付与するのは仕様違反ではないが非推奨とのこと。支援技術や組み合わせによっては読み上げられなかったりするそうです。
もしもspan
やdiv
にアイコンフォントやCSSアイコンを表現するのであれば、role="img"
を併記するとaria-label
も確実に読まれるようになるそうです。
<span class="my-accordion__tabicon" role="img" aria-label="タブを開閉する"></span>
付け加えるとこういった場合はvisually-hiddenクラスで画面から隠すアプローチの方が良さそうです。
.visually-hidden {
/**
* a11y-css-resetから引用
* https://github.com/mike-engel/a11y-css-reset
**/
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
}
修正後
<h2 class="my-accordion__headline">
<button type="button"
class="my-accordion__tab js-accordion-tab"
aria-expanded="false"
aria-controls="accordion-panel-number-1">
見出し
<span class="my-accordion__tabicon"></span>
</button>
</h2>
アコーディオンのパネルにWAI-ARIAを導入してみる
続いてパネルにWAI-ARIAを付与していきます。
<div id="accordion-panel-number-1"
class="my-accordion__panel js-accordion-panel"
aria-hidden="true"
aria-labelledby="accordion-tab-number-1">
<div class="my-accordion__panel-content">
パネルのコンテンツ
</div>
</div>
解説
- パネルを制御しているタブの
aria-controls
の属性値と同じものをid
に付与します。これで影響を与える側と影響を受ける側を紐付けることが可能になりました。 -
aria-hidden="true"
を指定することで支援技術の読み上げ対象から除外します。 -
aria-labelledby
属性を利用してパネルのラベリングを行います。ラベルとなるテキストは制御しているタブの文言なのでaria-labelledby
の属性値に紐付いているタブのid
の属性値を含めます。
■参考記事:ARIA ラベルと関係性 | Web | Google Developers
レビューを受けて
1. aria-hidden="true"
と併せてdisplay: none
もしくはvisibility: hidden
を指定する
aria-hidden="true"
は支援技術の読み上げからは除外されますが、実態は存在しているため中身にリンクやボタンなどのフォーカスの当たる要素が存在するとタブフォーカスを受け取ってしまうとのこと。aria-hidden="true"
と合わせて display: none
もしくはvisibility: hidden
を指定して非表示にする ようにしましょう。今回はJSでコンテンツの高さを見ている関係でvisibility: hidden
を選択しました。
.my-accordion__panel[aria-hidden="true"] {
visibility: hidden;
}
visibility: hidden
は用途があまりないと言われがちですが、display: none
が利用しにくい箇所を読み上げ時にも隠す場合には積極的に取り入れていきたいプロパティだなと。
2. aria-labelledby
属性は不要
パネルのaria-labelledby
属性は不要という指摘をいただきました。理由としては
- マークアップ的に直上に見出しが存在している
-
role
があるわけでもなくラベルが求められていることもない
とのこと。
修正後
<div id="accordion-panel-number-1"
class="my-accordion__panel js-accordion-panel"
aria-hidden="true">
<div class="my-accordion__panel-content">
パネルのコンテンツ
</div>
</div>
アコーディオンのHTMLが完成
ざっとWAI-ARIAを付与して作られたアコーディオンのHTMLがこちらです。めっちゃ大変。
<div class="my-accordion js-accordion">
<section class="my-accordion__item">
<h2 class="my-accordion__headline">
<button type="button"
id="accordion-tab-number-1"
class="my-accordion__tab js-accordion-tab"
aria-expanded="false"
aria-controls="accordion-panel-number-1">
見出し
<span class="my-accordion__tabicon" aria-label="タブを開閉する"></span>
</button>
</h2>
<div id="accordion-panel-number-1"
class="my-accordion__panel js-accordion-panel"
aria-hidden="true"
aria-labelledby="accordion-tab-number-1">
<div class="my-accordion__panel-content">
文章
</div>
</div>
</section>
...
</div>
ここからCSSとJSを書かないといけないんですねー…。
レビューを受けての修正後
<div class="my-accordion js-accordion">
<section class="my-accordion__item">
<h2 class="my-accordion__headline">
<button type="button"
class="my-accordion__tab js-accordion-tab"
aria-expanded="false"
aria-controls="accordion-panel-number-1">
見出し
<span class="my-accordion__tabicon"></span>
</button>
</h2>
<div id="accordion-panel-number-1"
class="my-accordion__panel js-accordion-panel"
aria-hidden="true">
<div class="my-accordion__panel-content">
文章
</div>
</div>
</section>
...
</div>
role="tab"は付与しなくていいの?
先程少し触れましたがアコーディオン"タブ"なのだからタブ箇所にrole="tab"
とパネルにrole="tabpanel"
、そしてアコーディオンコンポーネントのルートにrole="tablist"
を付与したほうがいいのでは?と思いましたが、どうやら現在は付ける必要は無さそうです。
というのも、現在のW3Cのドキュメントを読むと、以前はrole="tab"
関連の記述があったにも関わらず、現在はアコーディオンのrole="tab"
関連は軒並み削除されているからです。
- 現在のW3Cのアクセシビリティを意識したアコーディオンの例:https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html
- 以前のW3Cのアクセシビリティを意識したアコーディオンの項目:https://www.w3.org/TR/2013/WD-wai-aria-practices-20130307/#accordion
アクセシビリティが意識されているBootstrapのコンポーネントでもアコーディオンにrole="tab"
は付与されていませんでしたからね…ここらへんはアクセシビリティ有識者に伺いたいところです。
CSSとJSを書いていく
僕なりの注意点は以下です。
CSS
- Tab移動時にタブにフォーカスが当たっていることが確認できるようにする。今回は
:focus-visible
のPolyfillを導入してキーボード操作"以外"でフォーカスが当たったときのoutlineを消しています。 - タブの見出しが改行してもいいようにCSSを指定しましょう。固定値の
height
は使いません。
JS
- パネルが開いた時→タブの
aria-expanded
をtrue
に、パネルのaria-hidden
をfalse
(もしくはaria-hidden
属性自体を取り除く)に指定しましょう。 - パネルが閉じた時→タブの
aria-expanded
をfalse
に、パネルのaria-hidden
をtrue
に指定しましょう。 -
resize
イベントなりを使ってパネルの高さに変動があった時の高さの再計算を行いましょう。height
が固定されるので改行などが生じるとテキストが途切れてしまいます。ちなみにパネルにJSでheight
(サンプルだとmax-height
ですが)を指定している場合は``height: autoだと
transition`でアニメーションできないからです。 - 「開くパネルは一つだけ(一つ開いたら他は閉じる)」か「複数のパネルを開ける」かは設定でオンオフできるようにしておくと良いと思います。
ちなみにサンプルのJSはES6なのでそのままではIEでは動かないです。トランスパイルなどしましょう。
おわりに
ただ開閉できるアコーディオンを作るのは容易いけど、アクセシビリティを意識して作ると見るところが多くなって非常に大変だなと思った。アクセシビリティを意識してWeb制作に取り組んでいる方々はすごいと思います(語彙力不足)。
記事の内容とかサンプルとかツッコミどころ多いかもしれませんが、指摘・コメントはDiscussionにて頂ければ嬉しいです。
それでは。