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を0にセットし、短い遅延(20ms)の後に規定の高さまで開くという具合です。

20msより短いと開閉アニメーションがスムーズにならない場合があります。

閉じる時は、現在の高さから短い遅延の後にmax-heightを0にセット、そしてopen属性を取り除けば良いのですが、完全に閉じてからopen属性を取り除かないとアニメーションが即座に終わってしまうため、一番最後に削除しています。

連打防止

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

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

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-heightis-openクラスが付与されると、80pxまでの範囲ではアニメーションがスムーズに行われますが、アニメーション終了後styleが削除された時に、実際の高さ100pxに戻りカクつく現象が生じます。

この問題を改善するためにbox-sizing:content-boxを設定し、開いた状態でも閉じた状態でも、上下のpaddingを含めない80pxの高さを制御するという理屈です。

これにより、パディングが高さの計算に影響を与えず、スムーズなアニメーションが実現されます。

HISA

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

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

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

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

重要:対策しないとスクリーンリーダーが認識しなくなる

.details-content {
	overflow: hidden;
	min-height: 0.1px; /* 高さが0になるとスクリーンリーダーに認識されなくなるため */
}

コンテンツをoverflow:hiddenを指定していると、高さがなくなり、スクリーンリーダーに認識されず読まれなくなってしまいます。

Chromeのページ内検索でも文字にハイライトが当たらなくなります。

0でなければ認識されるようになるため、min-heightでアニメーションに影響のない高さを確保し対策しています。

HISA

原因が分かるまで数日かかりました

余談:CSSについて

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

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

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

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

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

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

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

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

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

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

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

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

おすすめWEBスクール

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

\ ここから飛べます! /

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

お気軽にコメントどうぞ

コメントする

目次