details / summaryで作るアコーディオンアニメーション
セマンティックなdetails / summaryタグを使用して、アコーディオンアニメーションを実装する方法を紹介します。
今回作るアコーディオンは、以下のガイドラインに従って作成しました。
- フォーカスの際にアウトラインが表示されているか
- テキストがスクリーンリーダーに読まれているか
- キーボードで適切に操作できるか
- ページ内検索で自動展開するか
これらは元々details / summaryに組み込まれている機能ですが、アニメーションを重視するあまりこれらの機能が失われてしまっている実装例もよく見受けられます。
それならdetails / summaryを使う意味がないですよね…
アニメーションもさせたいけど、同時に機能も妨げたくないという方は、是非参考にしてみてください。
details / summaryアコーディオン
See the Pen details / summary accordion +icon by hisa (@hisaaashi) on CodePen.
ポイント
- アクセシビリティ/ユーザビリティを妨げない
- ブラウザ間で一貫した動作を確保する
- アイコンを独自のものにする
- detalsのインナー1つだけで高さを制御させる
特に最後の、インナー1つだけで高さを制御させるという点にとても苦労しましたが、今回試行錯誤した結果、目標通りに完成させることができました。
開閉の仕組み
前提として、デフォルトのopen属性で開閉を制御するのではなく、JavaScriptからis-open
クラスを付与して開閉を制御しています。
Safariでは、open属性を付与させてからでないと高さを取得出来ないため(display:none
のような状態)、オープン時最初にセットします。
高さを取得した後、max-heightを0にセットし、短い遅延(20ms)の後に規定の高さまで開くという具合です。
20msより短いと開閉アニメーションがスムーズにならない場合があります。
閉じる時は、現在の高さから短い遅延の後にmax-heightを0にセット、そしてopen属性を取り除けば良いのですが、完全に閉じてからopen属性を取り除かないとアニメーションが即座に終わってしまうため、一番最後に削除しています。
連打防止
アニメーション中に何度もクリックすると、動作が乱れてしまうため、isAnimating
が true
の時はクリックイベントを受け付けないようにリターンして処理を終了しています。
オープン中だけでも動作が安定していたので、クローズ時は特に設定していません。
open属性とis-openクラスを同期
open属性が存在する場合、それに応じてis-open
クラスを追加し、open属性が存在しない場合にはis-open
クラスも削除する動作を関数にまとめています。
この関数はページの読み込み時に呼び出され、コンテンツを最初から開いた状態で表示したい場合、open属性を追加するだけで済むようになります。
また、details/summaryタグはページ内検索時に自動的にopen属性が追加され、閉じたコンテンツが自動的に表示される仕組みですが、今回はis-open
クラスを適用しないと表示されないため、toggleイベントが発生する際にもこの関数を使って同期させるようにしました。
重要:高さの取得方法
.details-content
に直接paddingを指定するとオープン時にカクついてしまうため、divを二重にして子要素の方にpaddingを指定する方法をよく見かけます。
しかし、HTMLとしては少し冗長なため、div1つで制御したいところです。
<details class="details">
<summary class="summary">タイトルが入ります</summary>
<div class="details-content">
<div class="details-content-inner">
<p>アコーディオンのコンテンツが入ります</p>
</div>
</div>
</details>
↓
<details class="details">
<summary class="summary">タイトルが入ります</summary>
<div class="details-content">
<p>アコーディオンのコンテンツが入ります</p>
</div>
</details>
試行錯誤した結果、box-sizing:content-box
に切り替えるというアプローチで解決できました。
/* コンテンツ */
.details-content {
box-sizing: content-box;
padding-block: 0;
}
/* コンテンツ(オープン時) */
.is-open .details-content {
padding-block: 20px;
}
- border-box:高さにpaddingが含まれる
- content-box:高さにpaddingが含まれない
paddingを含むとなぜカクつくか
オープン中に取得できる高さは、is-open
クラスが付与される前の高さ(上下のpaddingは0)です。
つまり、実際のオープン状態の高さが100pxで上下のpaddingが20pxある場合、閉じた状態から取得できる高さは80pxになります。
max-height
とis-open
クラスが付与されると、80pxまでの範囲ではアニメーションがスムーズに行われますが、アニメーション終了後style
が削除された時に、実際の高さ100pxに戻りカクつく現象が生じます。
この問題を改善するためにbox-sizing:content-box
を設定し、開いた状態でも閉じた状態でも、上下のpaddingを含めない80pxの高さを制御するという理屈です。
これにより、パディングが高さの計算に影響を与えず、スムーズなアニメーションが実現されます。
言語化が非常に難しいのですが、分かりますか…?笑
さらに重要なポイントとして、高さを指定する際にはheightではなくmax-heightを使用することが挙げられます。
height
は確定的な値を指定するのに対し、max-height
は可変性を保ちつつも一定の制約を設ける方法です。
max-height
を使用しないと、滑らかなアニメーションが得られないことに注意が必要です。
重要:対策しないとスクリーンリーダーが認識しなくなる
.details-content {
overflow: hidden;
min-height: 0.1px; /* 高さが0になるとスクリーンリーダーに認識されなくなるため */
}
コンテンツをoverflow:hidden
を指定していると、高さがなくなり、スクリーンリーダーに認識されず読まれなくなってしまいます。
Chromeのページ内検索でも文字にハイライトが当たらなくなります。
0でなければ認識されるようになるため、min-heightでアニメーションに影響のない高さを確保し対策しています。
原因が分かるまで数日かかりました
余談:CSSについて
アイコンの前後の入れ替え
アイコン共通のgrid-column
の値を1か2にするだけで、位置をタイトルの前後選べるようにしています。
閉じるときのアニメーション
コンテンツが閉じる時のtransition
はpadding .2s ease .1s
というように.1s
だけ遅延させて閉じるようにしています。
これにより、わずかな時間の遅延で高さのみが閉じ、コンテンツは縮小されないように見せる効果を生み出しています。
詳細ブロックをアコーディオンにさせる方法
WordPress6.3から追加された詳細ブロック(details / summary)を、こちらのコードを応用してアコーディオン化する方法を記事にしました。
良かったら覗いてみてください。
まとめ:予想以上に大変だった
完成すれば「そんなことか」となるのですが、原因を探るまでかなりの時間を要しました。
今回の要となるのは、box-sizing:content-box
とmax-height
の指定です。
何か気づいたことや感想などあればお気軽にコメントくださいませ。
おすすめWEBスクール
WEB制作やWEBデザインを学びたいなら、SNSでも話題の「デイトラ
どのコースも10万円前後と業界最安値で、副業や転職に向けて十分なスキルを身につけることができます。
\ ここから飛べます! /
お気軽にコメントどうぞ