開閉・展開

アコーディオン

解説あり

Accordion

見出しを押すとパネルが開閉する UI。ボタンと aria-expanded で「今開いているか」を支援技術へ正しく伝えます。

アコーディオンとは?

アコーディオンは、見出し(ヘッダー)を押すと、その下のパネルが開いたり閉じたりする UI です。 FAQ、設定画面、商品詳細など「普段は隠しておき、必要なときだけ開きたい」場面でよく使われます。

見た目はシンプルですが、「いま開いているのか・閉じているのか」を、目で見えない人にも伝える必要があります。ここがアクセシビリティの肝です。

なぜアクセシビリティが大事なの?

2種類のユーザーを想像すると分かりやすいです。

この2つを満たす最短ルートが、「見出しを <button> にして、aria-expanded で状態を伝える」ことです。

ライブデモ(推奨実装)

下のアコーディオンは APG に沿って実装したものです。マウスを使わず、ぜひキーボードだけで操作してみてください。

アクセシブルなアコーディオン

ご注文から2〜4営業日でお届けします。配送状況はマイページの「注文履歴」から確認できます。

試してみよう:Tab でボタンに移動 → Enter / Space で開閉 → ↑ ↓ で見出し間を移動、Home / End で最初・最後へ。

ポイント

スクリーンリーダー(macOSなら +F5 で VoiceOver)をオンにすると、 ボタンにフォーカスしたとき「折りたたみ, 配送について, 展開済み」のように役割と状態が読み上げられます。これが aria-expanded の効果です。

キーボード操作

キー動作必須/任意
Enter / Spaceフォーカスしている見出しのパネルを開く/閉じる必須
Tab次のフォーカス可能要素(次の見出しなど)へ移動必須
/ 次 / 前の見出しへフォーカス移動任意(推奨)
Home / End最初 / 最後の見出しへフォーカス移動任意

補足

EnterSpace での開閉は、見出しを <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-expandedfalse にしてパネルを hidden にします。 ただし すべて閉じられるようにしておくのが親切です(APG でも推奨)。

アンチパターン(Bad)

下は <div>onclick だけで作った「見た目だけ同じ」アコーディオンです。マウスでは動きますが、キーボードでは一切操作できません。上のデモと同じように TabEnter を試して、違いを体感してください。

div で作った壊れたアコーディオン
配送について
返品・交換は可能ですか?
使える支払い方法は?

試してみよう: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> を使えば、その大半がタダで手に入ります。

実装チェックリスト


原文(英語):Accordion Pattern — W3C APG(新しいタブで開きます)