アコーディオン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 属性を付けると初期状態で展開されます。<details open> と書くだけで、JavaScriptなしで初期展開が実現します。排他制御(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: 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ハッシュ初期展開・複数同時展開など、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() でイベントの親への伝播を止めることで、独立した動作が実現できます。アクセシビリティ完全対応版
スクリーンリーダーやキーボード操作のユーザーにも使いやすいアコーディオンを実装します。aria-expanded・aria-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');
}
});
});
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');
});
});
slideDown/slideUpはjQueryがJavaScriptでstyleを毎フレーム書き換えるため、大量の項目が連動すると処理が重くなる場合があります。CSSの
max-height トランジション+クラス付与の組み合わせはGPUで処理されるため、特にモバイル端末でパフォーマンスが向上します。実装方法の比較と選び方
| 方法 | JS不要 | アニメーション | 排他制御 | アクセシビリティ | 向いている用途 |
|---|---|---|---|---|---|
| <details>/<summary> | ◎ | △(ブラウザ依存) | ✗ | △(自動対応) | 最小構成・静的ページ |
| CSSのみ(max-height) | ◎ | ○ | △(工夫必要) | ✗(要追加) | AMP・超軽量ページ |
| jQuery slideToggle() | ✗ | ◎ | ◎ | ○(追加実装) | 汎用・FAQ・一般的なUI |
| jQueryアクセシビリティ完全版 | ✗ | ◎ | ◎ | ◎ | 業務アプリ・政府サイト・WCAG準拠 |
アニメーション品質・排他制御のしやすさ・コードの読みやすさのバランスが最も良いのがjQueryのslideToggle()です。アクセシビリティが重要な用途では本記事のaria属性版を使ってください。JavaScriptを一切使いたい場合は
<details> が最善の選択です。まとめ
アコーディオンの実装方法は用途によって使い分けが重要です。基本のjQuery実装から始め、要件に応じてアクセシビリティ対応やパフォーマンス最適化を加えていくアプローチを推奨します。
関連記事: アコーディオンの実装完全ガイド(排他制御・アイコン回転・URLハッシュ) / アニメーション完全ガイド(animate・slideToggle) / 初回アクセス時のみアコーディオンを開く完全ガイド
よくある質問(FAQ)
<details> はJavaScriptなしでもコンテンツが表示されるため、JavaScript無効環境での表示性は有利です。UXとSEOのバランスを考えるとjQuery実装でもデメリットはほぼありません。slideDown() のコールバックで $("html, body").animate({ scrollTop: $btn.offset().top - 80 }, 300) を呼ぶと、開いたパネルのヘッダー位置にスクロールします。固定ヘッダーがある場合はオフセット値(-80)を調整してください。<meta name="viewport" content="width=device-width"> を設定するだけでタップ遅延は解消されます。古いブラウザに対応する必要がある場合はFastClickライブラリか touch-action: manipulation CSSを使ってください。aspect-ratio: 16/9 や固定高さを設定してコンテンツの高さを事前に確定させるか、slideDown()の代わりに animate({ height: "toggle" }) を試してください。