【jQuery】アコーディオン実装方法の完全比較|details要素・CSSのみ・jQuery・ネスト・アクセシビリティまで

アコーディオンUIは「HTML標準の <details> 要素」「CSSのみ(JavaScriptなし)」「jQueryのslideToggle()」の3つの方法で実装できます。本記事ではそれぞれの特徴と使い分けを解説し、ネスト(入れ子)・アクセシビリティ完全対応・大量項目のパフォーマンス最適化まで踏み込みます。

この記事でわかること

  • <details>/<summary>(ブラウザネイティブ)の実装と限界
  • CSSのみで実装するアコーディオン(JavaScriptなし)
  • jQueryのslideToggle()による実装と696との連携
  • ネスト(入れ子)アコーディオンの実装
  • aria-expanded・aria-controls・キーボード操作の完全アクセシビリティ対応
  • 100件以上のパネルを持つ大量項目のパフォーマンス最適化
  • 3つの実装方法の比較テーブルと選び方
スポンサーリンク

<details>/<summary>で実装する(ブラウザネイティブ)

HTML標準の <details> 要素を使えば、JavaScriptなしでアコーディオンが実装できます。最もシンプルで、SEO的にもコンテンツがHTMLに直接含まれるためクローラーに優しいです。

<details>
  <summary>Q. 返品・交換はできますか?</summary>
  <p>商品到着後7日以内であれば返品・交換を承ります。</p>
</details>

<details open>
  <summary>Q. 配送はどのくらいかかりますか?</summary>
  <p>通常2〜3営業日でお届けします。</p>
</details>
/* details要素のスタイリング */
details {
  border: 1px solid #d1d5db;
  border-radius: 6px;
  margin-bottom: 8px;
  overflow: hidden;
}

summary {
  padding: 14px 16px;
  cursor: pointer;
  background: #f8fafc;
  font-weight: 600;
  list-style: none;  /* デフォルトの▶を非表示 */
  user-select: none;
}

summary::after {
  content: "+";
  float: right;
}

details[open] summary::after {
  content: "-";
}

details > p {
  padding: 12px 16px;
  margin: 0;
}
open属性で初期展開を制御
open 属性を付けると初期状態で展開されます。<details open> と書くだけで、JavaScriptなしで初期展開が実現します。
details要素の限界
排他制御(1つ開いたら他を閉じる)・スムーズなスライドアニメーション・独自のキーボード操作カスタマイズは<details>単体では困難です。これらが必要な場合はCSSトランジションまたはjQueryで実装してください。

CSSのみで実装する(JavaScriptなし)

チェックボックスハックや :focus-within を使ったCSS単体の実装はJavaScriptを読み込まない環境(AMP・SSRなど)で有効です。ここでは max-height トランジションを使った実装を紹介します。

<!-- チェックボックスハックを使うCSS-onlyアコーディオン -->
<div class="css-acc">
  <input type="checkbox" id="acc1" class="css-acc-toggle">
  <label for="acc1" class="css-acc-header">セクション1</label>
  <div class="css-acc-body">
    <p>セクション1の内容です。</p>
  </div>
</div>

<div class="css-acc">
  <input type="checkbox" id="acc2" class="css-acc-toggle">
  <label for="acc2" class="css-acc-header">セクション2</label>
  <div class="css-acc-body">
    <p>セクション2の内容です。</p>
  </div>
</div>
.css-acc-toggle {
  display: none;  /* チェックボックス本体は非表示 */
}

.css-acc-header {
  display: block;
  padding: 14px 16px;
  background: #f1f5f9;
  cursor: pointer;
  font-weight: 600;
  border: 1px solid #d1d5db;
  margin-bottom: 2px;
}

.css-acc-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}

/* チェックされたときにmax-heightを広げる */
.css-acc-toggle:checked + .css-acc-header + .css-acc-body {
  max-height: 500px;  /* コンテンツの最大高さより大きい値を設定 */
}
max-heightの値はコンテンツ高さより大きくする
max-height: 500px の値はコンテンツの実際の高さより大きければよく、小さいとコンテンツが切れます。ただし閉じるアニメーションが遅くなるため、大きすぎる値も避けてください。動的な高さが必要な場合はjQueryの slideToggle() が適しています。

jQueryのslideToggle()で実装する(基本)

slideToggle() はコンテンツの実際の高さを自動計算してアニメーションするため、CSSの max-height 問題がありません。排他制御(1つ開いたら他を閉じる)も自然に実装できます。

<div class="jq-acc">
  <div class="jq-header">セクション1</div>
  <div class="jq-body">
    <p>セクション1の内容です。</p>
  </div>
</div>
<div class="jq-acc">
  <div class="jq-header">セクション2</div>
  <div class="jq-body">
    <p>長いコンテンツでも高さを自動計算します。</p>
  </div>
</div>
$(function () {
  // 初期状態: 全て閉じる
  $('.jq-body').hide();

  $('.jq-header').on('click', function () {
    var $body = $(this).next('.jq-body');
    var isOpen = $body.is(':visible');

    // 排他制御: 他を全て閉じる
    $('.jq-body').slideUp(200);
    $('.jq-header').removeClass('is-open');

    // クリックしたものが閉じていれば開く
    if (!isOpen) {
      $body.slideDown(200);
      $(this).addClass('is-open');
    }
  });
});
排他制御・アイコン回転・URLハッシュ対応の詳細は696で
アイコン回転・URLハッシュ初期展開・複数同時展開など、jQueryアコーディオンの応用パターンはアコーディオンの実装完全ガイドで詳しく解説しています。

ネスト(入れ子)アコーディオンの実装

FAQ のカテゴリを折りたたみ、その中に個々のQ&Aがある「2段階アコーディオン」のような入れ子構造の実装です。クリックイベントの伝播(バブリング)対策がポイントです。

<div class="nest-acc outer">
  <div class="nest-header outer-header">カテゴリ: 配送について</div>
  <div class="nest-body outer-body">
    <div class="nest-acc inner">
      <div class="nest-header inner-header">Q. 配送日数は?</div>
      <div class="nest-body inner-body"><p>通常2〜3営業日です。</p></div>
    </div>
    <div class="nest-acc inner">
      <div class="nest-header inner-header">Q. 送料はかかりますか?</div>
      <div class="nest-body inner-body"><p>5,000円以上で送料無料です。</p></div>
    </div>
  </div>
</div>
$(function () {
  $('.nest-body').hide();

  // 外側アコーディオン
  $('.outer-header').on('click', function (e) {
    e.stopPropagation();  // 内側への伝播を防ぐ
    $(this).next('.outer-body').slideToggle(200);
    $(this).toggleClass('is-open');
  });

  // 内側アコーディオン
  $('.inner-header').on('click', function (e) {
    e.stopPropagation();  // 外側への伝播を防ぐ
    var $body  = $(this).next('.inner-body');
    var isOpen = $body.is(':visible');
    // 同じ外側の内側アイテムを排他制御
    $(this).closest('.outer-body').find('.inner-body').slideUp(150);
    $(this).closest('.outer-body').find('.inner-header').removeClass('is-open');
    if (!isOpen) {
      $body.slideDown(150);
      $(this).addClass('is-open');
    }
  });
});
e.stopPropagation()でクリックの伝播を止める
内側ヘッダーをクリックすると、イベントが親要素(外側ヘッダー)にもバブリングして外側も開閉してしまいます。e.stopPropagation() でイベントの親への伝播を止めることで、独立した動作が実現できます。

アクセシビリティ完全対応版

スクリーンリーダーやキーボード操作のユーザーにも使いやすいアコーディオンを実装します。aria-expandedaria-controls・キーボードナビゲーションが必要です。

<div class="a11y-acc">
  <button class="a11y-header"
          aria-expanded="false"
          aria-controls="panel1"
          id="btn1">
    セクション1
  </button>
  <div class="a11y-body" id="panel1" role="region" aria-labelledby="btn1" hidden>
    <p>セクション1の内容です。</p>
  </div>
</div>

<div class="a11y-acc">
  <button class="a11y-header"
          aria-expanded="false"
          aria-controls="panel2"
          id="btn2">
    セクション2
  </button>
  <div class="a11y-body" id="panel2" role="region" aria-labelledby="btn2" hidden>
    <p>セクション2の内容です。</p>
  </div>
</div>
$(function () {
  var $headers = $('.a11y-header');

  $headers.on('click', function () {
    var $btn    = $(this);
    var $panel  = $('#' + $btn.attr('aria-controls'));
    var isOpen  = $btn.attr('aria-expanded') === 'true';

    // 他を閉じる(アニメーション後にhidden属性を付与)
    $headers.not($btn).attr('aria-expanded', 'false');
    $headers.not($btn).each(function () {
      var $p = $('#' + $(this).attr('aria-controls'));
      $p.slideUp(200, function () { $p.attr('hidden', true); });
    });

    // 対象を開閉
    if (isOpen) {
      $btn.attr('aria-expanded', 'false');
      $panel.slideUp(200, function () { $panel.attr('hidden', true); });
    } else {
      $btn.attr('aria-expanded', 'true');
      $panel.removeAttr('hidden').hide().slideDown(200);
    }
  });

  // キーボード操作: ↑↓で前後のヘッダーにフォーカス移動
  $headers.on('keydown', function (e) {
    var idx   = $headers.index(this);
    var total = $headers.length;
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      $headers.eq((idx + 1) % total).trigger('focus');
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      $headers.eq((idx - 1 + total) % total).trigger('focus');
    } else if (e.key === 'Home') {
      e.preventDefault();
      $headers.first().trigger('focus');
    } else if (e.key === 'End') {
      e.preventDefault();
      $headers.last().trigger('focus');
    }
  });
});
WAI-ARIAアコーディオンの必須属性

  • aria-expanded="true/false": ヘッダーボタンに。開閉状態をスクリーンリーダーに伝える
  • aria-controls="panelId": ヘッダーが制御するパネルのIDを指定
  • role="region": パネルがセクションであることを示す
  • aria-labelledby="btnId": パネルのラベル(ヘッダーボタン)を参照
  • hidden属性: display:noneに相当。aria的に「存在しない」扱い

大量項目のパフォーマンス最適化

アコーディオンのパネルが100件以上ある場合、全件にイベントをバインドするとメモリ使用量が増加します。イベント委譲で1つの親要素にバインドするのが効果的です。

$(function () {
  // BAD: 全てのヘッダーにイベントを個別バインド(100件なら100回登録)
  // $('.acc-header').on('click', function() { ... });

  // GOOD: 親要素1つにイベント委譲(何件でも1回の登録)
  $('#acc-container').on('click', '.acc-header', function () {
    var $body  = $(this).next('.acc-body');
    var isOpen = $body.is(':visible');

    $('#acc-container .acc-body').slideUp(150);
    $('#acc-container .acc-header').removeClass('is-open');

    if (!isOpen) {
      $body.slideDown(150);
      $(this).addClass('is-open');
    }
  });

  // slideUp/Downが重い場合はCSSトランジション+クラス切り替えが高速
  $('#acc-container').on('click', '.fast-header', function () {
    var $acc = $(this).closest('.fast-acc');
    $acc.siblings().removeClass('is-open');
    $acc.toggleClass('is-open');
  });
});
大量件数ではCSSトランジション+クラス切り替えが有利
slideDown/slideUpはjQueryがJavaScriptでstyleを毎フレーム書き換えるため、大量の項目が連動すると処理が重くなる場合があります。CSSの max-height トランジション+クラス付与の組み合わせはGPUで処理されるため、特にモバイル端末でパフォーマンスが向上します。

実装方法の比較と選び方

方法 JS不要 アニメーション 排他制御 アクセシビリティ 向いている用途
<details>/<summary> △(ブラウザ依存) △(自動対応) 最小構成・静的ページ
CSSのみ(max-height) △(工夫必要) ✗(要追加) AMP・超軽量ページ
jQuery slideToggle() ○(追加実装) 汎用・FAQ・一般的なUI
jQueryアクセシビリティ完全版 業務アプリ・政府サイト・WCAG準拠
結論: 迷ったらjQuery slideToggle()
アニメーション品質・排他制御のしやすさ・コードの読みやすさのバランスが最も良いのがjQueryのslideToggle()です。アクセシビリティが重要な用途では本記事のaria属性版を使ってください。JavaScriptを一切使いたい場合は <details> が最善の選択です。

まとめ

アコーディオンの実装方法は用途によって使い分けが重要です。基本のjQuery実装から始め、要件に応じてアクセシビリティ対応やパフォーマンス最適化を加えていくアプローチを推奨します。

関連記事: アコーディオンの実装完全ガイド(排他制御・アイコン回転・URLハッシュ) / アニメーション完全ガイド(animate・slideToggle) / 初回アクセス時のみアコーディオンを開く完全ガイド

よくある質問(FAQ)

Q<details>要素とjQueryアコーディオンはどちらがSEOに有利ですか?
ASEO的には大きな差はありません。Googleは両方のコンテンツをクロール・インデックスします。ただし <details> はJavaScriptなしでもコンテンツが表示されるため、JavaScript無効環境での表示性は有利です。UXとSEOのバランスを考えるとjQuery実装でもデメリットはほぼありません。
QWordPressでCocoonテーマを使っています。アコーディオンを実装するには?
ACocoonにはbb-accordion(ブロックエディター用)が内蔵されていますが、カスタムデザインや細かい挙動制御が必要な場合は本記事のjQueryコードを子テーマのfunctions.phpでエンキューしたJSファイルに記述してください。
Qアコーディオンを開いたときに上にスクロールしたいです。
AslideDown() のコールバックで $("html, body").animate({ scrollTop: $btn.offset().top - 80 }, 300) を呼ぶと、開いたパネルのヘッダー位置にスクロールします。固定ヘッダーがある場合はオフセット値(-80)を調整してください。
Qスマートフォンでタップすると反応が遅い(300ms遅延)場合の対処法は?
Aモダンなブラウザでは <meta name="viewport" content="width=device-width"> を設定するだけでタップ遅延は解消されます。古いブラウザに対応する必要がある場合はFastClickライブラリか touch-action: manipulation CSSを使ってください。
Qアコーディオン内に動画やiframeを入れたいのですが、高さが正しく計算されません。
AslideDown()はコンテンツが読み込まれた後の高さで動作するため、動画やiframeがまだ読み込まれていないと正しく計算されないことがあります。CSSで aspect-ratio: 16/9 や固定高さを設定してコンテンツの高さを事前に確定させるか、slideDown()の代わりに animate({ height: "toggle" }) を試してください。