みなさん、サイト制作で、モーダルとかアコーディオンって作ったことありますか?
この、何かを隠して、また表示して、また隠す。(ハンバーガーメニューとかもそう)
こういう系のUIって、実装するときめちゃめちゃ考えること多いですよね。(これに限ったことじゃないけど)
今回はアクセシビリティ的な観点から
サイト上で参戦率が高い上記2つのUIにフォーカスを当て
マークアップする際、どんなことに気をつけたら良いか、備忘録的にまとめてきました。
1. モーダル
必要な基本機能
モーダルの実装をする前に、一度押さえておきたい基本機能をリストアップします。
- モーダル表示のトリガー要素をクリック、またはフォーカスが当たっている時に
enter
かspace
ボタンでモーダルが表示される - モーダルが表示されたら、モーダル内にフォーカスが移る
- スクリーンリーダーもモーダル内のコンテンツを読み上げてくれる
- モーダルが起動中は背景はスクロールできなくなる(時と場合によるかも)
- モーダルを閉じる時は閉じるボタンか背景クリックか
escape
- モーダルが閉じた時、モーダルを開く前の場所にフォーカスが戻っている
- モーダルの中身が見えていない時は、スクリーンリーダーを無視させる
フォーカスのケアが結構大変かも。
気をつけよう、キーボード操作
モーダル起動中のキーボード操作は下記のようにまとめられます。(MDNから引用)
- tab
- モーダル内の次のフォーカス可能な要素にフォーカスを移動
- フォーカスがモーダル内の最後のフォーカス可能な要素にある場合、フォーカスをモーダル内の最初のフォーカス可能な要素に移動
- shift + tab
- モーダル内の前のフォーカス可能な要素にフォーカスを移動
- モーダル内の最初のフォーカス可能な要素にフォーカスがある場合、モーダル内の最後のフォーカス可能な要素にフォーカスを移動
- escape
- モーダルを閉じる
なんだか周りくどいですが
要はモーダルが開いたら、フォーカスは、閉じるまでモーダル内で回遊するということです。
いざ実装! できるだけ楽したい!
ここまで、モーダルの基本機能やキーボード操作をまとめました。
モーダル開くボタン置いて〜、これ押したらモーダルの中身を display: block;
にして〜
モーダルの中の閉じるボタンおしたら display: none;
にしよ♫
じゃ、絶対ケアできないですね。
とはいえ、はじめから自分で全部実装するのは果てしないので
上記の基本機能を押さえたモーダルを、いかに楽に実装するかを考えましょう。
方法は2つあると思います。
1. ライブラリに頼る
私も、私の周りもよくMicromodal.jsを利用しています。
採用される理由は主にこれです。
- JQuery非依存
- WAI-ARIAのガイドラインに準拠していてアクセシブル
- 軽量
Micromodal.jsのドキュメントにも書いてありますが
基本機能はこちらです。
- オーバーレイクリックでモーダルを閉じる
-
esc
ボタンを押すとモーダルを閉じる -
aria-hidden
属性でモーダルの 表示 / 非表示 切り替え - モーダル内でのタブ フォーカスのトラッピング(前述した、フォーカスがモーダル内だけで回遊するやつ)
- モーダル切り替え前後でフォーカス位置を維持する
- モーダル内の最初のフォーカス可能な要素にフォーカスする
じゃじゃ〜ん
さっき言ったやつ全部盛りですね。
さてさて、どんなHTML構造なのかな。。。
<!-- [1] -->
<div id="modal-1" aria-hidden="true">
<!-- [2] -->
<div tabindex="-1" data-micromodal-close>
<!-- [3] -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-1-title" >
<header>
<h2 id="modal-1-title">
Modal Title
</h2>
<!-- [4] -->
<button aria-label="Close modal" data-micromodal-close></button>
</header>
<div id="modal-1-content">
Modal Content
</div>
</div>
</div>
</div>
ふむふむ。
ほぼdiv
要素で構成されていて、aria
属性でそのdiv
がなんの役割をしているのかマシンに理解らせてる感じですね。
[1]のaria-hidden
でモーダルが表示されていない時はスクリーンリーダーが読み飛ばすようにしています。モーダルが表示された時、ここがfalseになります。
[3]のrole="dialog"
でこれがダイアログ要素だと明示しています。そしてaria-modal="true"
でこれがモーダルであると明示しています。
ん??これってダブルミーニングでは?
と思いましたが、aria-modal
属性でその要素がモーダルであることをスクリーンリーダーなどの支援技術に示し、それ以外のコンテンツの利用を排除する (インタラクション可能な範囲を、モーダルのコンテンツに限定する) というものです。
aria-labelledby
はモーダルのタイトルですね。
[4]のaria-label
はモーダル内でどんな役割を担った要素なのかを明示します。これが入っているとスクリーンリーダーはたとえbutton
の中にテキストが入っていたとしても読み飛ばし、aria-label
の中のテキストを読み上げます。
よくある x アイコンだけの閉じるボタンとかには、スクリーンリーダーユーザー向けに必ず記述しておきましょう。
WAI-ARIAの仕様に準拠したHTML構造なのはわかりましたが、アクセシブルにするために忘れちゃいけない記述が多いなあという印象です。
2. dialog要素を使ってみる
dialog
要素とは、ダイアログボックスや、消すことができるアラート、インスペクター、サブウィンドウ等のような対話型コンポーネントを表します。
WAI-ARIAの仕様にも、モーダルを実装するときはdialog
要素で実装したほうがより一層セマンティックになって良いかも!と書いてました。
特徴としてはopen
属性をもってて、このつけ外しで表示非表示を行います。
なので、最初から表示させとくモーダルとかにはopen="open"
みたいにしとくってことですね。
<dialog id="js-dialog" class="dialog _1" open="open">
<div aria-labelledby="ダイアログのタイトル">
<p>ダイアログのコンテンツ</p>
<button id="js-dialog-close" class="button" value="close" aria-label="ダイアログを閉じる">dialogを閉じる</button>
</div>
</dialog>
疑似要素も備わっていて、backdrop
疑似要素にbackground-color
を付けてあげるだけで背景色が指定できます。
.dialog {
&::backdrop {
background-color: rgba(43, 62, 80, 0.7);
}
}
3種類のメソッドも備わっていて、show()
とshowModal()
がopen属性を付与しモーダルを表示させます。
両者の違いは、show()
だとモーダル意外も操作可能なモーダル
showModal()
だとモーダルの裏側のコンテンツは操作できなくなります。
close()
はそのままで、このメソッドを実行するとopen属性が取り除かれモーダルが閉じます。
const modal = document.getElementById('js-dialog');
const open = document.getElementById('js-dialog-open');
const close = document.getElementById('js-dialog-close');
open.addEventListener('click', () => {
showModal();
});
close.addEventListener('click', () => {
closeModal();
});
各ブラウザの実装状況はこちら
FirefoxやSafariは2022年の春頃実装されたばっかりのバージョンなので
まだdialog
要素が機能しないブラウザを利用しているユーザーがいることに注意が必要です。
なんだ残念、まだ使えないじゃん…
そんな時のためにGoogleが未実装ブラウザのためのポリフィルを提供してくれてました。
https://github.com/GoogleChrome/dialog-polyfill
dialog
要素の基本機能をまとめると
- キーダウン操作がケアできる
- フォーカス移動のケアができる
- 非表示の時、スクリーンリーダーが読み飛ばしてくれる
- 表示された時、スクリーンリーダーがモーダルが表示されたと理解してくれる(モーダルであると読み上げてくれる)
といった感じで、特別なaria
属性をたくさん付与しなくても
簡単にアクセシブルなモーダルが実装できちゃいました。
ただ注意したいのが
フォーカス移動において
Micromodal.jsは完全にモーダル内でのみ回遊するのに対し
dialog要素はモーダル内のフォーカスできる最後の要素に達した時、次にtab
キーを押すとブラウザのURL入力欄に飛びます。
これは使い勝手的にメリットになる場合とデメリットになる場合があると思うので
一概にデメリットとは言えない気がします。(みなさんはどう思いますか?)
上記以外にも気をつけることはあって
- 現段階(2022年12月)ではポリフィルを入れる必要がある
- 背景クリックでモーダルが閉じない
- open属性のつけ外しで表示非表示を判定するため、アニメーションが効かない
といった問題があります。
背景クリックや、表示非表示時のアニメーションは自前のJSが必要です。
まとめ
ここまで、なるべく楽にアクセシブルなモーダルを実装するための機能と実装方法を紹介しました。
楽に、という点にフォーカスを当てるとライブラリに頼る方に軍配があがりそうです。
ところで、こんな記述をみつけました。
https://developer.mozilla.org/ja/docs/Learn/Accessibility/WAI-ARIA_basics
一点忘れてはいけないのが、 WAI-ARIA は必要な場合のみ使用するという点です。 理想的には、スクリーンリーダーのユーザーの理解に必要となる意味論の提供は、常に ネイティブの HTML 機能 を使用して行うべきです。 しかし、コードの制御が限定されていたり、 HTML 要素への実装が容易ではない複雑なものを作っているなどの理由で、これが困難となるケースがあります。 そのような場合、 WAI-ARIA はアクセシビリティを向上させる上で価値のあるツールとなります。
もう一度言いますが、必要な時だけ使ってください!
これはMDN、「WAI-ARIAの基本」というページの「いつ WAI-ARIA を使うべき?
」という項目に記載されています。
必要な時だけ使ってってめっちゃ念押してくる。
やっぱり基本はネイティブのHTML機能を使用するべきなんですね。
脳死でdivでつくって後からaria属性つけたしまくるなよってことです。
Micromodal.jsのHTML構造を紐解くと
dialog
要素の機能に寄せられるようになんとかdiv
にaria
属性をいっぱいつけて頑張っている感じがするので
これからの実装は上記記載の点も含めてdialog
要素でのマークアップが主流になってくるのではないかと考えます。
2. アコーディオン
必要な基本機能
さて、次はこちらもサイト内で頻出のUI、アコーディオンについて
基本機能を見ていきましょう。
-
enter
またはspace
- 折りたたまれたパネルのアコーディオン ヘッダーにフォーカスがある場合、関連するパネルを展開します。実装が 1 つのパネルのみの展開を許可し、別のパネルが展開されている場合は、そのパネルを折りたたみます。
- 展開されたパネルのアコーディオン ヘッダーにフォーカスがある場合、実装が折りたたみをサポートしている場合は、パネルを折りたたみます。一部の実装では、常に 1 つのパネルを展開する必要があり、展開できるパネルは 1 つだけです。そのため、折りたたみ機能はサポートされていません。
-
tab
- フォーカスを次のフォーカス可能な要素に移動します。アコーディオン内のすべてのフォーカス可能な要素がページTabシーケンスに含まれます。
-
shift
+tab
- フォーカスを前のフォーカス可能な要素に移動します。アコーディオン内のすべてのフォーカス可能な要素がページTabシーケンスに含まれます。
-
↓
(オプション)- フォーカスがアコーディオン ヘッダーにある場合、フォーカスを次のアコーディオン ヘッダーに移動します。フォーカスが最後のアコーディオン ヘッダーにある場合、何もしないか、フォーカスを最初のアコーディオン ヘッダーに移動します。
-
↑
(オプション)- フォーカスがアコーディオン ヘッダーにある場合、フォーカスを前のアコーディオン ヘッダーに移動します。フォーカスが最初のアコーディオン ヘッダーにある場合、何もしないか、フォーカスを最後のアコーディオン ヘッダーに移動します。
-
home
(オプション)- フォーカスがアコーディオン ヘッダーにある場合、フォーカスを最初のアコーディオン ヘッダーに移動します。
-
end
(オプション)- フォーカスがアコーディオン ヘッダーにある場合、フォーカスを最後のアコーディオン ヘッダーに移動します。
なにやらめちゃめちゃたくさんあります。
今回はオプションのところは割愛しましょう…
いざ実装!HTML構造、どうしよう…
1. input要素
方法1つ目はinput
要素でマークアップする方法です。
エンジニア歴1年の私が言うのもなんですが、ちょっと古いイメージがあります。
<div class="acd-area">
<input id="acd-check1" class="acd-check" type="checkbox">
<label class="acd-label q" for="acd-check1">好きな食べ物はなんですか?</label>
<div class="acd-content">
<p>オムライスとカレーとお寿司と餃子です</p>
</div>
<input id="acd-check2" class="acd-check" type="checkbox">
<label class="acd-label q" for="acd-check2">最近ハマっているお酒は何ですか?</label>
<div class="acd-content">
<p>ハイボールとシャリキンバイスサワーです</p>
</div>
</div>
input
要素でつくる旨味はjsがいらないことです。
type
属性をcheckbox
にしたりradio
にしたりして、チェックされているかどうかをcssで判定して
アコーディオンコンテンツの表示非表示を行います。アニメーションもcssでできますね。
ただ、コードぱっと見でこれらがアコーディオンであるとちょいとわかりにくいです。
そしてキーボードフォーカスできないのでいくらenter
やspace
を押しても受け付けてくれません。
スクリーンリーダーも開閉状態を読み上げてくれないです。
2. dl dt dd要素
次にdl
dt
dd
要素です
<div class="acd-area">
<dl>
<dt class="acd-label acd-cursor q" aria-expanded="false" aria-controls="acd-panel-1">
好きな食べ物はなんですか?
</dt>
<dd class="acd-target" aria-hidden="true" id="acd-panel-1">
オムライスとカレーとお寿司と餃子です
</dd>
</dl>
<dl>
<dt class="acd-label acd-cursor q" aria-expanded="false">
最近ハマっているお酒は何ですか?
</dt>
<dd class="acd-target" aria-hidden="true">
ハイボールとシャリキンバイスサワーです
</dd>
</dl>
</div>
こちらは見慣れていることもあり、コードをみただけでアコーディオンだとわかりやすいです。(個人の感想です)
そして、スクリーンリーダー対応のためにたくさんaria
属性が必要になります。
aria-expanded
でアコーディオンが開いているか、閉じているかを理解らせます。
ラベルに記載したaria-controls
と、アコーディオンコンテンツのid
を揃えることで両者が紐づきます。
アコーディオンコンテンツは最初は閉じているのでaria-hidden
はtrue
にして、開いたらfalse
に書き換えましょう。
aria
属性の書き換えはjsで行うため、htmlもめんどくさければjsもめんどくさいですね。
(ただ開閉アニメーションはjQueryのslideToggleつかえばあっという間!)
そしてdt要素は普通tab
キーではフォーカスされないので、キーボード操作可能にするには
tabindex="0"
をつけてフォーカスが当たった時にenter
かspace
が押されたら開く、みたいなjsを書くか
div
とbutton
要素の組み合わせとかでマークアップするのがいいかもしれないです。
3. details要素とsummary要素
最後にdetails
要素とsummary
要素のご紹介です
<div class="acd-area">
<details>
<summary>好きな食べ物は何ですか?</summary>
オムライスとカレーとお寿司と餃子です
</details>
<details>
<summary>最近ハマっているお酒は何ですか?</summary>
ハイボールとシャリキンバイスです
</details>
</div>
この要素で実装するメリットはコードがとてもシンプルになるということ
あと何も考えなくてもキー操作できるし
フォーカスも当たるし
スクリーンリーダーも開閉状態を判別できます。
なによりの旨味は
サイト内検索時にも、非表示中のコンテンツに検索文字列があればアコーディオンが開いてハイライトしてくれます。
各種ブラウザの実装状況も問題なさそうです。
https://caniuse.com/?search=details
ただ、こちらもdialog
要素と同じようにopen
属性で開閉するのでアニメーションが効きません。
ちなみに、このままではsafariでlist-style
が有効になってしまい
ブラウザ依存の三角アイコン*が表示されてしまうので、この記述を忘れないようにしないといけないです。
summary::-webkit-details-marker {
display: none;
}
蛇足
ちなみに、details
とsummary
を使えば
アコーディオンが閉じててもサイト内検索でひっかかって勝手に開くと言いましたが
hidden="until-found"
属性を付与すれば
アコーディオンが閉じていても、隠された単語を見つけ、かつアコーディオンを開くことができます。
やったー!!
と言いたいところですが、今のところChromeとEdgeにしか対応してなかったです涙
https://caniuse.com/?search=until-found
なにやら、この属性を付与された要素にはbeforematch
というイベントが使えるようになり、
このイベント内で、サイト内検索でひっかかった時にアコーディオンを開いてごにょごにょ…
的なjsを書けばdetails
とsummary
要素のような実装ができるらしいです。
興味のある方はこちら!
https://developer.mozilla.org/en-US/docs/Web/API/Element/beforematch_event
私も引き続き勉強します。
まとめ、 というか感想
ここまで、アコーディオンをつくるのに必要な機能と実装方法を紹介しました。
details
とsummary
要素使ったことある方いらっしゃいますか?
できればネイティブなHTMLの機能を使って実装したいけど、なんだか痒いところに手が届かないなというのが個人の感想です。
結局のところ、アコーディオンに関してはどんな実装方法をとっても
アクセシブルにしようとすればどこかで苦しい思いをしなければならないので
どの要素を選択するのかは、デザインによるのかもしれないです。
アコーディオンがサイト上で登場する理由第1位は、コンテンツが長くなるから。
その意味合いを考えるとdetails
とsummary
は英単語の意味からとってもあまり適してない気がします。
逆にQ & Aのセクションだったり、大きなセクションの詳細部分に使うのであれば使っても良いかもしれないです。
モーダルを実装する場合にも言えることですが
マークアップする際は使用する要素が、デザイン上の意味合いと合致しているかを意識しながら実装していけるよう
日々勉強が必要だなと思いました。