details / summaryで作るアコーディオンアニメーション

details / summaryで作るアコーディオンアニメーション

セマンティックなdetails / summaryタグを使用して、アコーディオンアニメーションを実装する方法を紹介します。

今回作るアコーディオンは、以下のガイドラインに従って作成しました。

  • フォーカスの際にアウトラインが表示されているか
  • テキストがスクリーンリーダーに読まれているか
  • キーボードで適切に操作できるか
  • ページ内検索で自動展開するか

これらは元々details / summaryに組み込まれている機能ですが、アニメーションを重視するあまりこれらの機能が失われてしまっている実装例もよく見受けられます。

HISA

それなら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を指定することでアニメーションにも影響なく、スクリーンリーダーに読まれるようになりました。

連打防止

アニメーション中に何度もクリックすると、動作が乱れてしまうため、isAnimatingtrueの時はクリックイベントを受け付けないようにリターンして処理を終了しています。

オープン中だけでも動作が安定していたので、クローズ時は特に設定していません。

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部分を切り分けて制御できるという理屈です。

HISA

言語化が非常に難しいのですが、分かりますか…?笑

さらに重要なポイントとして、高さを指定する際にはheightではなくmax-heightを使用することが挙げられます。

heightは確定的な値を指定するのに対し、max-heightは可変性を保ちつつも一定の制約を設ける方法です。

max-heightを使用しないと、滑らかなアニメーションが得られないことに注意が必要です。

余談:CSSについて

アイコンの前後の入れ替え

アイコン共通のgrid-columnの値を1か2にするだけで、位置をタイトルの前後選べるようにしています。

閉じるときのアニメーション

コンテンツが閉じる時のtransitionpadding .2s ease .1sというように.1sだけ遅延させて閉じるようにしています。

これにより、わずかな時間の遅延で高さのみが閉じ、コンテンツは縮小されないように見せる効果を生み出しています。

詳細ブロックをアコーディオンにさせる方法

WordPress6.3から追加された詳細ブロック(details / summary)を、こちらのコードを応用してアコーディオン化する方法を記事にしました。

良かったら覗いてみてください。

まとめ:予想以上に大変だった

完成すれば「そんなことか」となるのですが、原因を探るまでかなりの時間を要しました。

今回の要となるのは、box-sizing:content-boxmax-heightの指定です。

何か気づいたことや感想などあればお気軽にコメントくださいませ。

おすすめWEBスクール

WEB制作やWEBデザインを学びたいなら、SNSでも話題の「デイトラ」がおすすめ!
どのコースも10万円前後と業界最安値で、副業や転職に向けて十分なスキルを身につけることができます。

役に立ったら他の方にシェア

お気軽にコメントどうぞ

コメントする

目次