開閉・展開
アコーディオン
解説ありAccordion
見出しを押すとパネルが開閉する UI。ボタンと aria-expanded で「今開いているか」を支援技術へ正しく伝えます。
アコーディオンとは?
アコーディオンは、見出し(ヘッダー)を押すと、その下のパネルが開いたり閉じたりする UI です。 FAQ、設定画面、商品詳細など「普段は隠しておき、必要なときだけ開きたい」場面でよく使われます。
見た目はシンプルですが、「いま開いているのか・閉じているのか」を、目で見えない人にも伝える必要があります。ここがアクセシビリティの肝です。
なぜアクセシビリティが大事なの?
2種類のユーザーを想像すると分かりやすいです。
- キーボードだけで操作する人(マウスが使えない・使いにくい)。 見出しが
<div>だと Tab でフォーカスできず、そもそも開けません。 - スクリーンリーダーを使う人(画面を音声で読む)。 ただの
<div>は「ボタン」と認識されず、開閉できることも、いま開いているかも分かりません。
この2つを満たす最短ルートが、「見出しを <button> にして、aria-expanded で状態を伝える」ことです。
ライブデモ(推奨実装)
下のアコーディオンは APG に沿って実装したものです。マウスを使わず、ぜひキーボードだけで操作してみてください。
試してみよう:Tab でボタンに移動 → Enter / Space で開閉 → ↑ ↓ で見出し間を移動、Home / End で最初・最後へ。
ポイント
スクリーンリーダー(macOSなら ⌘+F5 で VoiceOver)をオンにすると、 ボタンにフォーカスしたとき「折りたたみ, 配送について, 展開済み」のように役割と状態が読み上げられます。これが aria-expanded の効果です。
キーボード操作
| キー | 動作 | 必須/任意 |
|---|---|---|
| Enter / Space | フォーカスしている見出しのパネルを開く/閉じる | 必須 |
| Tab | 次のフォーカス可能要素(次の見出しなど)へ移動 | 必須 |
| ↓ / ↑ | 次 / 前の見出しへフォーカス移動 | 任意(推奨) |
| Home / End | 最初 / 最後の見出しへフォーカス移動 | 任意 |
補足
Enter と Space での開閉は、見出しを <button> にするだけで自動的に得られます(自分でキー処理を書く必要はありません)。これがネイティブ要素の強みです。
必要な WAI-ARIA / ロール
| 付ける場所 | 属性 / ロール | 意味 |
|---|---|---|
| 見出しのボタン | aria-expanded="true | false" | 対応するパネルが開いているか。状態の変化に合わせて必ず更新する。 |
| 見出しのボタン | aria-controls="パネルのid" | どのパネルを制御するボタンかを示す。 |
| 見出しの外側 | <h2>〜<h4> | ページの構造に合った見出しレベルで囲む。スクリーンリーダーの見出しジャンプに役立つ。 |
| パネル | role="region" + aria-labelledby="見出しのid" | パネルを名前付きの領域にする(任意だが推奨。領域が多すぎると逆効果なので数が多いときは省略可)。 |
| 閉じているパネル | hidden | 閉じている間はDOMから隠し、キーボード/読み上げの対象外にする。 |
実装:推奨パターン(Good)
良い例 / 推奨
見出しを <button> にし、aria-expanded を状態と同期させます。
マークアップ:
<div class="accordion">
<h3>
<button type="button"
id="acc-h-1"
aria-expanded="true"
aria-controls="acc-p-1">
配送について
</button>
</h3>
<div id="acc-p-1"
role="region"
aria-labelledby="acc-h-1">
ご注文から2〜4営業日でお届けします。
</div>
<h3>
<button type="button"
id="acc-h-2"
aria-expanded="false"
aria-controls="acc-p-2">
返品・交換は可能ですか?
</button>
</h3>
<div id="acc-p-2"
role="region"
aria-labelledby="acc-h-2"
hidden>
商品到着後14日以内であれば承ります。
</div>
</div>開閉のスクリプト(状態の同期がすべて):
const triggers = document.querySelectorAll('.accordion button');
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(
trigger.getAttribute('aria-controls')
);
// 状態を反転して aria-expanded と表示を同期させる
trigger.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
});補足
「同時に1つだけ開く」アコーディオンにしたい場合は、開く前に他のボタンのaria-expanded を false にしてパネルを hidden にします。 ただし すべて閉じられるようにしておくのが親切です(APG でも推奨)。
アンチパターン(Bad)
下は <div> と onclick だけで作った「見た目だけ同じ」アコーディオンです。マウスでは動きますが、キーボードでは一切操作できません。上のデモと同じように Tab → Enter を試して、違いを体感してください。
試してみよう:Tab を押しても見出しにフォーカスが当たりません。スクリーンリーダーでは「ボタン」とも認識されず、開閉できることすら伝わりません。
<!-- ❌ アンチパターン -->
<div class="accordion">
<!-- div なのでキーボードでフォーカスできない -->
<div class="trigger" onclick="toggle('p1')">
配送について +
</div>
<div id="p1" style="display:none">
ご注文から2〜4営業日でお届けします。
</div>
</div>悪い例 / 避ける
この実装の問題点:
- キーボードで操作できない —
divはフォーカスを受け取れない。 - 役割が伝わらない — スクリーンリーダーには「ただのテキスト」に聞こえ、押せると分からない。
- 状態が伝わらない —
aria-expandedがなく、開/閉が判別できない。 display:noneではなくhidden属性 / 適切な管理がない(インラインstyle頼み)。
ポイント
どうしても <div> を使うなら、role="button"・tabindex="0"・Enter/Space のキー処理・aria-expanded をすべて自前で足す必要があります。 最初から <button> を使えば、その大半がタダで手に入ります。
実装チェックリスト
- 見出しのトリガーは
<button type="button">である - ボタンは見出し要素(
<h2>〜<h4>)で囲まれている aria-expandedが開閉と常に一致しているaria-controlsでパネルのidを指している- 閉じているパネルは
hiddenでキーボード/読み上げから外れている - パネルに
role="region"+aria-labelledby(任意・推奨) - キーボードだけで開閉でき、フォーカスが見える(フォーカスリングを消していない)
- (任意)↑↓ Home End で見出し間を移動できる