アコーディオン
クリックで項目が開閉し、アイコンが回転。一度に1つだけ開く排他制御で高さを抑える。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- FAQ・仕様一覧など見出しだけ先に見せたい一覧
- 縦の長さを抑えつつ本文を選んで読ませたい
- 1つずつ読めばよい排他的な項目群
- 避ける場面
- 複数項目を同時に見比べる比較内容
- 本文が長く開閉でスクロール位置が飛ぶ項目
- 必ず読ませたい重要情報を畳む
- 構成要素
- 項目
- 見出しボタン
- 開閉アイコン
- 本文
- 持っている状態
- 通常
- 押下中
- キーボード
Tab で各見出しボタンへ・Enter/Space で開閉(button 標準)。開くと他の項目は閉じる
- 支援技術
APG Accordion: 見出し button に aria-expanded と aria-controls で本文(id 付き)を結び、開閉に同期。+が×に回転し色以外でも開閉が分かる(アイコンは aria-hidden)
- 動き
grid-template-rows 0fr→1fr を 0.28s ease で高さを連続的に開閉・アイコン回転 0.25s。reduced-motion では即時
デザインの要点
- 高さは grid-template-rows 0fr→1fr で開閉する。scrollHeight を測って max-height に入れる方式より短く、内容が増えても値の手直しが要らない
- 見出しは button 要素にする。div に click を付けると Tab で届かず、aria-expanded を付けても読み上げが「ボタン」と言わない
- +を45°回して×にする。開閉は色だけでなく形でも分かり、閉じる操作がどこにあるかを同じ場所で示せる
- 排他制御(1つ開くと他が閉じる)は仕様ではなく設計判断。比較して読ませたい内容なら外す
使いどころ
FAQ・仕様一覧など「見出しは知りたいが本文は選んだものだけ読みたい」場面に向く。1項目ずつしか開かない排他制御なので、全体の見通しの良さ(縦に並ぶ見出しの一覧性)を保ったまま、開いたページの高さを最小限に抑えられるのが利点。逆に、複数項目を同時に見比べたい用途(比較表など)には排他制御が邪魔になるため、その場合はJSの「全て閉じてから開く」処理を外し、複数同時に開けるアコーディオンに作り替える必要がある。また、開閉のたびに直前まで読んでいた項目が閉じてスクロール位置がずれるため、極端に長い本文を持つ項目を混ぜると体験が悪化する。本文が長くなりがちな内容は、アコーディオンではなく別ページや別セクションに切り出す方がよい。
仕組み
高さのアニメーションを grid-template-rows: 0fr → 1fr の変化で実現しているのが要点。ac-body を1行のグリッドにし、行のサイズを0frと1frの間でトランジションさせることで、中の ac-content の実際の高さを都度JSで測らなくても、CSSだけで「0から本来の高さまで」の滑らかな開閉ができる。開閉の排他制御は、クリックされた項目が開いていたかを wasOpen で先に記録し、いったん全項目の is-open を外してから、閉じていた項目だけ改めて is-open を付け直すという単純な総入れ替えで実装している。アイコンは「+」の文字を45度回転させて「×」に見せているだけで、開閉自体はクラストグルのみで完結する。
実装の注意
grid-template-rows のトランジションによる高さアニメーションは、scrollHeight を測ってmax-heightを操作する旧来のJSテクニックより簡潔だが、比較的新しいCSSの挙動に依存しているため、対応が必要な古いブラウザを含むプロジェクトでは事前に動作確認しておく必要がある。もう一つ具体的な落とし穴として、.ac-content の padding-bottom は is-open クラスの有無だけで 0 と 12px を切り替えており、transition の対象に含まれていない。そのためgrid-template-rowsが滑らかに0fr⇄1frと変化する一方で、paddingはクリックした瞬間に数値だけ即座に変わり、開閉のごく初動でわずかにカクつく。気になる場合は padding もtransitionの対象に加えるか、paddingで余白を作るのをやめて ac-content 側に固定の下マージンを持たせる構成に変える。
アクセシビリティ
button 要素をヘッダーに使っているためTabでの到達とEnter/Spaceでの開閉というキーボード操作の基本はブラウザ標準で成立している。そのうえで WAI-ARIA の Accordion(Disclosure の集合)パターンに沿わせるため、各 .ac-header に aria-expanded="true"/"false" を持たせて開閉と同期し、対応する .ac-body に id を振って aria-controls で結びつけている(JS の setOpen() が is-open クラスと aria-expanded を必ず同時に書き換える)。これによりスクリーンリーダー利用者は「これは開閉できるボタンで、今開いているか閉じているか」を音声で把握できる。+の記号は装飾なので aria-hidden にし、ラベルが「配送について+」と読まれないようにしてある。排他的に1つしか開かない挙動自体はARIAの必須要件ではなく実装側の任意の設計だが、その場合でも「開くと他が閉じる」ことを事前に案内文などで示しておくと、支援技術での操作がより予測しやすくなる。
調整して使う
触る値は主に3つ。アイコンの表現(+回転/シェブロン回転/なし)、排他か複数同時開放か、そしてトランジションの時間(0.28s)。シェブロンに変える場合は「+を45度回してバツにする」回転角とは向きの計算が異なるので、アイコン素材ごとに回転角を作り直すこと。複数同時に開けるようにするには、クリック時に「全項目を閉じてから開く」処理をやめ、クリックされた項目自身の is-open だけをトグルする形に変えればよい。本文が長い項目とごく短い項目が混在する場合は、0.28sの固定時間だと長い項目だけ動きが遅く見えるため、実測の高さに応じて持続時間を可変にする(あるいは easing だけで速度感を揃える)と統一感が出る。
コード(コピーして使えます)
<ul class="accordion">
<li class="ac-item">
<button class="ac-header" aria-expanded="false" aria-controls="ac-body-1">
配送について
<span class="ac-icon" aria-hidden="true">+</span>
</button>
<div class="ac-body" id="ac-body-1">
<div class="ac-content">全国翌日配送対応です。離島・一部地域は2〜3日かかる場合があります。</div>
</div>
</li>
<li class="ac-item">
<button class="ac-header" aria-expanded="false" aria-controls="ac-body-2">
返品・交換ポリシー
<span class="ac-icon" aria-hidden="true">+</span>
</button>
<div class="ac-body" id="ac-body-2">
<div class="ac-content">お届けから14日以内であれば、未使用品に限り返品・交換を承ります。</div>
</div>
</li>
<li class="ac-item">
<button class="ac-header" aria-expanded="false" aria-controls="ac-body-3">
お支払い方法
<span class="ac-icon" aria-hidden="true">+</span>
</button>
<div class="ac-body" id="ac-body-3">
<div class="ac-content">クレジットカード、コンビニ払い、銀行振込に対応しています。</div>
</div>
</li>
</ul>