details要素のname属性を使用した排他的なアコーディオンの実装例
広告
details
要素のname
属性を使用した排他的なアコーディオンの実装メモです。
details
要素で新たに追加されたname
属性を使用すればJavaScriptを使用せずともWeb標準で排他的なアコーディオンを実現できます。
details要素のname属性とは
Open UIでの議論を発端として追加された、オープンされているdetails
要素をひとつに限定する(どれかのdetails
要素が開かれたら開いている他のdetails
要素は閉じる)排他的なアコーディオンを実現できる属性です。
使用方法はフォームパーツのname
属性同様に、排他的なアコーディオンを実装したいdetails
要素をname
属性でグループ化するだけです。name
属性にはユニークな値を指定できますが、空の文字列を使用するのは禁じられています。
上記のマークアップだけで次のような排他的なアコーディオンの実装を行うことができます。
現在のHTMLの仕様ではname
属性を使用して複数のdetails
要素をグループ化する場合には、details
要素とそれらの関連要素を包含要素 (section
要素やarticle
要素など) にまとめて保持することが推奨されています。
また、グループを見出しで紹介することが合理的である場合は、包含要素に見出し要素を配置するようにします。
実装時の注意点
details
要素のname
属性は低コストで排他的なアコーディオンを実装できるため非常に便利ですが、以下のような注意点があるため留意してください。
現時点で全モダンブラウザではサポートされていない
details
要素のname
属性は2024年4月時点ではFirefoxでサポートされていません。また、iOS含むSafariも17.2からサポートが開始されたばかりです。
従ってname
属性を使用するアコーディオンは現時点では移行期間であると認識しておいたほうがいいでしょう。
name
属性がサポートされていない環境でも排他的なアコーディオンを実現したい場合は、JSを使用してdetails
要素のopen
属性を切り替える必要があります。例えばChrome for Developersの文献では次のようなポリフィルが紹介されています。
もしくはname
属性がサポートされていなくてもdetails
要素の基本的な機能は失われないため、プログレッシブエンハンスメントの考え方に基づいてname
属性をそのまま導入することも選択肢の一つだと思います。
open属性はグループにひとつしか存在してはいけない
「排他的」なので当たり前の話ではありますが、同じname
属性値を指定したdetails
要素のグループに複数のopen
属性を指定することはNGです。
同じname
属性値を指定したdetails
要素のグループに複数のopen
属性を指定した場合、グループの最初のdetails
要素のみが開かれ、他のopen
属性が指定されたdetails
要素は閉じたままになります。また、JSで操作する場合も最後に操作されたdetails
要素のopen
属性のみが開いた状態になり、その他は問答無用で閉じられます。
このことは現在のHTMLの仕様にて以下のように記述されています。
The group of elements that is created by a common name attribute is exclusive, meaning that at most one of the details elements can be open at once. While this exclusivity is enforced by user agents, the resulting enforcement immediately changes the open attributes in the markup. This requirement on authors forbids such misleading markup.
つまり、同じname
属性値を持つdetails
要素のグループ内でopen
属性を複数指定することは仕様に反する誤解を招くマークアップであるとみなされます。また、この排他性はユーザーエージェントによって強制され、強制が適応される場合はマークアップ内のopen
属性が即座に変更されます。
同じname属性を内包してはいけない
次のマークアップのように同じname
属性を内包するのは仕様で禁止されています。
実際に上記のようなマークアップをした場合、子要素のdetails
要素を開くと親要素が閉じてしまうため、結果として子要素の中身を表示することができなくなります。
開閉のアニメーションを同時に行うことは仕様上不可能
前提として、details
要素に閉じる時のアニメーションを加える場合はアニメーションの終了を待ってからopen
属性を外すという処理が必要になります。
そのため、排他的なアコーディオンにおいてdetails
要素が開かれる時のアニメーションと他のdetails
要素が閉じる時のアニメーションを両立する場合、それぞれにopen
属性を持たせる必要があります。しかし、name
属性を用いる場合は先述した「open
属性はグループにひとつしか存在してはいけない」制約により不可能ということになります。
ただし、ある工夫をすれば開閉のアニメーションを両立することは可能です。name
属性を使用しつつ開閉のアニメーションを両立したい場合は次の実装例を参考にしてください。
開閉のアニメーションを両立した排他的なdetails要素の実装例
details
要素のname
属性を使用しつつ開閉のアニメーションを両立させる実装例です。当ブログで使用しているアコーディオンの実装を流用したものとなります。
排他的な機能が必要な場合はname
属性を持たせ、排他的な機能が不要であればname
属性を指定せずにそのままJSを適用してください。
任意のdetails
要素を開いて表示しておきたい場合はマークアップでopen
属性を指定してください。
マークアップ側の制約はdetails
要素の子要素はsummary
要素とmargin
padding
border
などのスタイルが当たっていないdiv
要素1つのみです。
いつもの通りJSの実装はGitHub Gistに纏めています。コードにはTypeScriptを使用していますので、TypeScriptを利用していないWeb制作現場で使用する場合はChatGPTなどに依頼してJSファイルに変換してください。
また、このJSを導入すればdetails
要素のname
属性がサポートされていない17.2未満のiOS SafariやFirefoxでも排他的アコーディオンを実現できますので、先述したポリフィルの導入は不要になります。
スタイリングに関してはsummary
要素のデフォルトの三角形のアイコンはデザイン反映においては不要になるため非表示にしておくと良いでしょう。Safari以外はUAスタイルシートで指定されているdisplay:list-item
を上書きすることで非表示にできますが、Safariは::-webkit-details-marker
疑似要素をdisplay:none
する必要があります。
他のスタイルは要件に依るため特筆することはありません。
アニメーションのポイント
Web Animations APIを使用する
今回の実装ではWeb Animations APIを使用してjQueryのslideToggle()
的なアニメーションを実現しています。
slideToggle()
的なアニメーションを行う際は予めパネル要素の高さの実寸値を取得しておく必要があります。
高さの取得にはscrollHeight
などを用いるのが主流だと思いますが、現在のCSSは原則的に論理プロパティで指定するのが定石であること、加えて当ブログのように縦書き対応も行っておきたいという理由からwindow.getComputedStyle()
でアニメーション適用前のパネル要素のblock-size
を取得するようにします。
アニメーションはblock-size
ではなく可変的でありつつも一定の制約を設けられるmax-block-size
をアニメーションさせるのがベターでしょう。
次に、アニメーションのオプションはinitializeDetailsAccordion()
の第二引数で受け取れるようにします。
duration
に負の値が差し込まれるとエラーになるのでMath.max()
関数で最小値を0としておきます。
そしてrequestAnimationFrame
を使用して、次の描画フレームでアニメーションを開始します。panel.animate(keyframes, animationOptions)
でキーフレームとオプションに基づいてアニメーションを作成し、animation.addEventListener('finish', onAnimationEnd)
を使用してアニメーションが終了したときにonAnimationEnd()
関数が呼び出されるようにします。
先述したように閉じる際はアニメーションの終了を待ってからopen
属性を取り外すようにします。
注意点として、いつものanimation
の指定の癖でfill
にforwards
とかboth
は指定しないようにしてください。アニメーション後もmax-block-size
の値が固定値のままだとレスポンシブ時の改行の有無などで表示に不具合が出ますし、閉じた後に高さが0
に固定されているとページ内検索時に中身がヒットした際に自動で開くというdetails
要素のメリットを殺すことになります。
開閉のアニメーションを両立させる
先述したようにname
属性を持たせたまま開閉のアニメーションを両立させるのは仕様上不可能です。
そのため、アニメーション開始時にname
属性を取り除き、アニメーション終了時にname
属性を復元することでやや強引に開閉のアニメーションを両立するようにします。
最初は初期化時にname
属性をdata-name
属性に置き換えることも検討しましたが、ページ内検索時に排他的で無くなるためアニメーション中だけ取り外すほうがいいという判断をしました。details
要素のname
属性がサポートされていない環境では検索時の排他性は失われますが、details
要素のページ内検索のサポートは現状name
属性がサポートされているChrome系のブラウザのみなので考えないこととします。
アニメーション中だけoverflow:clipを適用する
slideToggle()
的なアニメーションを実装する際はoverflow
プロパティを使用してコンテンツのはみ出しを防ぐ必要がありますが、今回の実装ではアニメーション中のみにoverflow:clip
を適用しています。
実装例ではパネル要素に十分なpadding
が設けられているため問題ありませんが、padding
が無いor小さい場合にoverflow:clip
が有効なままだと子要素のフォーカスリングが意図せずに切り取られてしまう可能性があります。
なるべくであればfocusableな要素を含む親要素に不必要なoverflow
プロパティの指定は避けたほうが良いため、万が一のリスクを避けるためにもアニメーション終了時にはoverflow
プロパティをリセットするようにしています。
また、あくまで要素のはみ出しを防ぐ目的のみであればoverflow:hidden
よりもoverflow:clip
を優先したほうが良いでしょう。overflow:clip
はプログラム的なスクロールを含むすべてのスクロールを禁止できるのに加えて、overflow:hidden
はスクロールコンテナを生成し、新しい整形コンテキストも作成する仕様が災いして予期せぬ不具合を引き起こす可能性があります。
マークアップ側の条件が増えてしまうため今回は使用しておりませんが、grid-template-rows
をアニメーションしてslideToggle()
的なアニメーションを行う場合はoverflow:hidden
でなければいけないなど、場合によってはoverflow:clip
ではなくoverflow:hidden
を選択するケースも存在します。
また、動的なコンテンツが差し込まれる場合はcontain:content
を指定するほうがパフォーマンス面で有利になるメリットがありますが、アニメーション中のみに適用するとガタツキが生じる場合があるのでフォーカスリングの問題は意識しつつ恒久的に指定しておいたほうがいいでしょう。
なお、grid-template-rows
のアニメーションについては過去に投稿した記事「アコーディオンのスライドアニメーションはCSS2行で実装できる」にて解説しています。
視差効果(アニメーション)を減らす設定がされている場合にはdurationを0にする
過去記事で何度か説明しているため詳細は省きますが、視差効果(アニメーション)を減らす設定がされている場合にはアニメーションを行わないようにします。
window.matchMedia('(prefers-reduced-motion: reduce)').matches
で設定がされているかを確認し、設定がされている場合はアニメーションの間隔を0
にします。
アニメーション中は連打を防止する
アニメーションの連打防止策としてグローバル変数isAnimating
を使用します。アニメーションが開始するとisAnimating
はtrue
に設定され、アニメーションが終了するとfalse
に戻ります。
summary
要素に登録したhandleClick
イベント内でisAnimating
がtrue
の時は早期リターンするようにします。
details
要素のアニメーションに関する他の記事では、アニメーション中のdetails
要素にカスタムデータ属性を持たせて連打防止フラグとする方法が紹介されています。しかし、複数のdetails
要素がアニメーション中にname
属性の復元タイミングがズレるとひとつのグループが複数のopen
属性を持つ可能性があります。
そのため、今回のケースではアニメーション中は他のアコーディオンの動作を封じる方が不具合の防止になるため、このような施策を行うようにします。
オプションで印刷時に全てのアコーディオンを開くようにする
排他的アコーディオン自体が持つデメリットとして、全てのコンテンツを開いて印刷することができないというものがあります。
印刷の問題に関してはOpen UIで議論があったようですが、印刷時の強制的な展開は行わない結論になったようです。
そのため、印刷する時は全てのアコーディオンを開いておいたほうが良さそうなコンテンツの場合はオプションで印刷時に全展開するようにします。
beforeprint
イベントが検知したら現在展開されているdetails
要素をdata-open-status
属性に保存した後にname
属性を取り除いて全展開します。その後、afterprint
イベントが検知したらname
属性を戻した後にdata-open-status
属性を参照して印刷前の展開状態を復元します。
ただし、アコーディオンメニューのように別に印刷しても嬉しくない(むしろ邪魔になり得る)要素の場合は、全展開しても余計な印刷が含まれるだけでなくコピー用紙の枚数やインクの量に負担を掛けることになるため、あくまでオプションで全展開がベストな場合以外にはfalse
にしておいたほうが良さそうです。
なんでもかんでもdetails要素を使えばいいってわけではない
以前投稿した記事「タブやアコーディオンの非表示コンテンツにはhidden=“until-found”を使うべし」で触れたように、ランドマークの下にトリガーとなる見出しと開閉コンテンツといった構成の場合はdetails
要素を使わずにhidden="until-found"
とaria-expanded
属性などで実装するほうが望ましいです。(理由はそちらの記事参照)
hidden="until-found"
を使用したアコーディオンの実装例に関しても「タブやアコーディオンの非表示コンテンツにはhidden=“until-found”を使うべし」にて紹介しています。そちらのコードも参考にしてください。
また、アコーディオンはユーザーに展開させる手間を与える機能であるため、本当にそのアコーディオンが必要か? は設計段階で考慮したほうがいいかもしれません。
参考リンク
- 4.11.1 The details element | HTML Standard
- Exclusive Accordion (Explainer)
<details name>
による排他的アコーディオンの実現 | 富永日記帳- 限定アコーディオン | CSS and UI | Chrome for Developers
- これは知っておくとかなり便利! details要素にname属性を与えると、連動して開閉するアコーディオンを実装できます | コリス
- details / summaryで作るアコーディオンアニメーション | ぶろぐみ
- detailsとsummaryタグで作るアコーディオンUI - アニメーションのより良い実装方法 - ICS MEDIA