FAQセクション
質問をクリックすると答えがなめらかに開くアコーディオン式のよくある質問。開いている項目のシェブロンが回転し、一度に複数開けます。
プレビュー
操作クリック/タップで動きます。
使いどころ
料金ページや製品トップに置く「よくある質問」のように、質問文を一覧で見せつつ回答は必要な人だけが開いて読む場面に向く。全問の回答を最初から表示すると縦に長くなりすぎるページで、質問文だけをスキャンして目的の答えにすぐ辿り着けるようにする効果がある。デモは複数の項目を同時に開いたままにできるアコーディオンで、1つ開くと他が閉じる排他的な実装ではない。関連性の低い独立した質問群(料金・解約・チームなど話題がバラバラ)には複数同時展開が向くが、手順を順に説明するような質問群では1つずつしか開かない排他制御にした方が読みやすくなる場合もある。問い合わせ件数が多い項目ほどリストの先頭に置くと発見されやすい。
仕組み
開閉はmax-heightのトランジションで実現している。閉じるときはまず現在の高さ(scrollHeight)をmax-heightに明示してからrequestAnimationFrameで次のフレームに0へ落とすことで、'auto'から'0'への変化ではアニメーションしないというCSSの制約を回避している。開くときも同様にscrollHeightを一度セットしてからtransitionでその高さまで伸ばし、transitionendイベントを拾ってmax-height: noneに解除することで、開いた後にウィンドウ幅が変わって内容の折り返しが変化しても高さが追従できるようにしてある。開閉状態はbutton要素のaria-expanded属性で管理し、クリックイベントはul要素側でイベント委任(e.target.closest('.faq-q'))してまとめて処理しているため、項目を動的に増やしてもJS側の変更は不要になる。
実装の注意
見落としやすいのは、開いている項目の答え文がページ内検索(ブラウザのCtrl+F)やSEOのクロールに対しては常にDOM上に存在している(display:noneではなくmax-height:0とopacity:0で隠しているだけ)ため、検索エンジンやページ内検索には最初から回答文が見えている点。回答文をクロール対象に含めたい場合には都合がよい一方(なおFAQリッチリザルト自体は2026年に廃止されており、表示枠を狙う目的はもう成立しない)、逆に「閉じているのに答えが見えている」ような視覚的な不具合(max-heightの計算がずれてはみ出すなど)が起きやすい実装でもあるため、質問文や回答文が動的に変わるケース(多言語切り替えなど)では高さの再計算漏れに注意する。またtransitionendはmax-height以外のプロパティ変化でも発火しうるため、e.propertyNameのチェック漏れがあると意図しないタイミングでmax-height: noneが呼ばれる可能性がある。
アクセシビリティ
開閉ボタンはbutton要素にaria-expandedとaria-controlsを持たせるパターンで実装されており、details/summaryを使う場合と比べてJSでの開閉制御やアニメーションの自由度が高い代わりに、aria-expandedの同期をJS側で確実に行う責任が実装者に残る。answer側にrole="region"を付けているが、WAI-ARIAのDisclosure(開閉)パターンでは必須ではなく、ラベルの無いregionランドマークを大量に作ると、ランドマークジャンプで一覧するユーザーにとって同じ名前のないregionが並ぶだけのノイズになりうる。ラベル無しで使うなら省略するか、aria-labelledbyでその質問文自体をregionの名前として関連付けると意図が明確になる。開閉のフォーカス位置はボタン自身に留まる設計で、これはDisclosureパターンとして適切だ。
調整して使う
調整しやすいのは3点。まず排他制御の有無で、他の項目を開くと自動的に他を閉じるアコーディオン(1つだけ開く)にしたいときは、クリック時に自分以外のis-openを外す処理を足すだけで切り替えられる。次に開閉トリガーの実装方式で、JSに依存させたくない静的サイトではbutton+aria-expandedの代わりにdetails/summary要素を使うと、ブラウザ標準の開閉機構とキーボード操作、検索エンジンへの見え方をJSなしで得られる代わりに開閉アニメーションの自由度は下がる。最後に見た目で、シェブロンの回転だけでなくプラス/マイナスアイコンの切り替えにする、質問文の左に番号を振るなど、サイトのトーンに合わせて装飾を変えられる。
コード(コピーして使えます)
<div class="stage">
<ul class="faq" id="faq">
<li class="faq-item">
<button class="faq-q" aria-expanded="false" aria-controls="faq-a1">
<span>無料プランでもずっと使えますか?</span>
<svg class="faq-chevron" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><polyline points="3,5 7,9 11,5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="faq-a" id="faq-a1" role="region">
<p>はい。無料プランは期間の制限なくご利用いただけます。プロジェクト数や保存容量に上限がありますが、基本機能はすべてお使いいただけます。</p>
</div>
</li>
<li class="faq-item">
<button class="faq-q" aria-expanded="false" aria-controls="faq-a2">
<span>いつでも解約できますか?</span>
<svg class="faq-chevron" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><polyline points="3,5 7,9 11,5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="faq-a" id="faq-a2" role="region">
<p>はい、いつでも解約できます。解約後も請求期間の終わりまでは有料機能をご利用いただけ、違約金は一切かかりません。</p>
</div>
</li>
<li class="faq-item">
<button class="faq-q" aria-expanded="false" aria-controls="faq-a3">
<span>チームで共有して使えますか?</span>
<svg class="faq-chevron" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><polyline points="3,5 7,9 11,5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="faq-a" id="faq-a3" role="region">
<p>Teamプランでメンバーを招待し、権限を分けて共同編集できます。監査ログやSSOにも対応しています。</p>
</div>
</li>
</ul>
</div>