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に1pxをセットし、短い遅延(20ms)の後に規定の高さまで開くという具合です。
20msより短いと開閉アニメーションがスムーズにならない場合があります。
閉じる時は、完全に閉じてからopen属性を取り除かないとアニメーションが即座に終わってしまうため、一番最後に削除しています。
補足:閉じた時の高さを0にしない
閉じた時の高さを0にセットするとスクリーンリーダーに認識されなくなります。
対策として、最小限の高さ1pxを指定することでアニメーションにも影響なく、スクリーンリーダーに読まれるようになりました。
連打防止
アニメーション中に何度もクリックすると、動作が乱れてしまうため、isAnimating
が true
の時はクリックイベントを受け付けないようにリターンして処理を終了しています。
オープン中だけでも動作が安定していたので、クローズ時は特に設定していません。
open属性とis-openクラスを同期
open属性が付いているのにis-open
クラスが付いていないのは矛盾が生じるので、同期処理を読み込み時とトグルイベントの時に実行しています。
これにより、ページ内検索で自動でopen属性が付与された時にはis-open
クラスが同期され、開閉アニメーションが行われるという仕組みです。
また、コンテンツを最初から開いておきたい場合にも、open属性を追加しておくだけで済むようになります。
重要:高さの取得方法
コンテンツ要素に直接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)です。
つまり、オープン状態の上下のpaddingが20pxならば、全体の高さが100pxあったとしても取得できる高さは80pxとなります。
そのため、80pxまでの範囲ではアニメーションがスムーズに行われますが、アニメーション終了後style
が削除された時に、実際の高さ100pxに戻りカクつく現象が生じます。
この問題を改善するためにbox-sizing:content-box
を設定し、取得できる高さにpaddingが含めないことで、コンテンツ部分とpadding部分を切り分けて制御できるという理屈です。
言語化が非常に難しいのですが、分かりますか…?笑
さらに重要なポイントとして、高さを指定する際にはheightではなくmax-heightを使用することが挙げられます。
height
は確定的な値を指定するのに対し、max-height
は可変性を保ちつつも一定の制約を設ける方法です。
max-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万円前後と業界最安値で、副業や転職に向けて十分なスキルを身につけることができます。
\ クリックしてジャンプ! /
お気軽にコメントどうぞ