【JavaScript】アコーディオンの作り方|Vanilla JS・CSS アニメーション・アクセシビリティ・FAQ パターンまで解説

【JavaScript】アコーディオンの作り方|Vanilla JS・CSS アニメーション・アクセシビリティ・FAQ パターンまで解説 JavaScript

アコーディオンはクリックでコンテンツが開閉する UI パターンで、FAQ ページやサイドメニューなどで広く使われます。jQuery を使わずVanilla JavaScript だけで、アニメーション付きのアコーディオンを実装する方法を解説します。

この記事でわかること
・HTML + CSS + Vanilla JS でアコーディオンを作る方法
・CSS transition でスムーズに開閉する方法
・1 つだけ開くパターン / 複数同時に開くパターン
・WAI-ARIA でアクセシビリティを確保する方法
・開閉アイコン(+ / -)の切り替え
・details / summary 要素との比較
・FAQ ページの実装パターン
スポンサーリンク

基本のアコーディオン(HTML + CSS + JS)

HTML

HTML
<div class="accordion">
  <div class="accordion-item">
    <button class="accordion-header">セクション 1</button>
    <div class="accordion-body">
      <div class="accordion-content">
        <p>セクション 1 の内容です。</p>
      </div>
    </div>
  </div>

  <div class="accordion-item">
    <button class="accordion-header">セクション 2</button>
    <div class="accordion-body">
      <div class="accordion-content">
        <p>セクション 2 の内容です。</p>
      </div>
    </div>
  </div>

  <div class="accordion-item">
    <button class="accordion-header">セクション 3</button>
    <div class="accordion-body">
      <div class="accordion-content">
        <p>セクション 3 の内容です。</p>
      </div>
    </div>
  </div>
</div>
ヘッダーには button 要素を使う
div や span ではなく <button> を使うことで、キーボード操作(Enter / Space で開閉)が自動的に対応され、スクリーンリーダーにも「ボタン」として認識されます。アクセシビリティの基本です。

CSS

CSS
.accordion-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 16px 20px;
  border: none;
  border-bottom: 1px solid #e2e8f0;
  background: #f8fafc;
  font-size: 16px;
  font-weight: 600;
  color: #1e293b;
  cursor: pointer;
  text-align: left;
}
.accordion-header:hover {
  background: #f1f5f9;
}

/* 開閉アニメーション: max-height + overflow で制御 */
.accordion-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}

.accordion-content {
  padding: 16px 20px;
}

/* 開閉アイコン */
.accordion-header::after {
  content: "+";
  font-size: 20px;
  font-weight: 400;
  transition: transform 0.3s ease;
}
.accordion-item.is-open .accordion-header::after {
  content: "\2212"; /* minus sign */
}
max-height + overflow: hidden でアニメーション
height: auto は CSS transition でアニメーションできません。max-height を 0 から scrollHeight(実際の高さ)に変化させることで、スムーズな開閉を実現します。

JavaScript

JavaScript(基本: 複数同時に開けるパターン)
document.querySelectorAll(".accordion-header").forEach(header => {
  header.addEventListener("click", () => {
    const item = header.parentElement;
    const body = header.nextElementSibling;

    // 開閉を切り替え
    if (item.classList.contains("is-open")) {
      // 閉じる
      body.style.maxHeight = null;
      item.classList.remove("is-open");
    } else {
      // 開く
      body.style.maxHeight = body.scrollHeight + "px";
      item.classList.add("is-open");
    }
  });
});

1 つだけ開くパターン(他を自動で閉じる)

JavaScript(排他的: 1 つだけ開く)
document.querySelectorAll(".accordion-header").forEach(header => {
  header.addEventListener("click", () => {
    const item = header.parentElement;
    const body = header.nextElementSibling;
    const isOpen = item.classList.contains("is-open");

    // 全アイテムを閉じる
    document.querySelectorAll(".accordion-item.is-open").forEach(openItem => {
      openItem.classList.remove("is-open");
      openItem.querySelector(".accordion-body").style.maxHeight = null;
    });

    // クリックしたアイテムが閉じていた場合のみ開く
    if (!isOpen) {
      body.style.maxHeight = body.scrollHeight + "px";
      item.classList.add("is-open");
    }
  });
});
パターン 動作 用途
複数同時に開ける クリックしたアイテムだけ開閉。他は影響なし FAQ ページ(複数の質問を同時に見たい場合)
1 つだけ開く クリックしたアイテムを開き、他は自動で閉じる サイドメニュー / ナビゲーション

WAI-ARIA でアクセシビリティを確保する

HTML(ARIA 属性付き)
<div class="accordion-item">
  <button class="accordion-header"
          aria-expanded="false"
          aria-controls="section1-body"
          id="section1-header">
    セクション 1
  </button>
  <div class="accordion-body"
       id="section1-body"
       role="region"
       aria-labelledby="section1-header">
    <div class="accordion-content">
      <p>セクション 1 の内容です。</p>
    </div>
  </div>
</div>
JavaScript(ARIA 属性の更新を追加)
header.addEventListener("click", () => {
  const item = header.parentElement;
  const body = header.nextElementSibling;
  const isOpen = item.classList.contains("is-open");

  // ARIA 属性を更新
  header.setAttribute("aria-expanded", !isOpen);

  if (isOpen) {
    body.style.maxHeight = null;
    item.classList.remove("is-open");
  } else {
    body.style.maxHeight = body.scrollHeight + "px";
    item.classList.add("is-open");
  }
});
ARIA 属性 対象
aria-expanded button(ヘッダー) true / false(開閉状態)
aria-controls button(ヘッダー) 制御する body 要素の id
role=”region” body(コンテンツ) 意味のある領域であることを示す
aria-labelledby body(コンテンツ) 対応するヘッダーの id
aria-expanded はスクリーンリーダーの必須属性
スクリーンリーダーは aria-expanded を読み上げて「展開済み」「折りたたみ」を伝えます。開閉時に setAttribute("aria-expanded", true/false) を更新してください。

完成版コード(コピペ可能)

JavaScript(完成版: ARIA + 1つだけ開く + アニメーション)
(() => {
  const headers = document.querySelectorAll(".accordion-header");
  if (!headers.length) return;

  headers.forEach(header => {
    header.addEventListener("click", () => {
      const item = header.parentElement;
      const body = header.nextElementSibling;
      const isOpen = item.classList.contains("is-open");

      // 他のアイテムを閉じる(排他的にする場合のみ)
      document.querySelectorAll(".accordion-item.is-open").forEach(openItem => {
        if (openItem !== item) {
          openItem.classList.remove("is-open");
          openItem.querySelector(".accordion-body").style.maxHeight = null;
          openItem.querySelector(".accordion-header").setAttribute("aria-expanded", "false");
        }
      });

      // クリックしたアイテムを切り替え
      if (isOpen) {
        body.style.maxHeight = null;
        item.classList.remove("is-open");
        header.setAttribute("aria-expanded", "false");
      } else {
        body.style.maxHeight = body.scrollHeight + "px";
        item.classList.add("is-open");
        header.setAttribute("aria-expanded", "true");
      }
    });
  });
})();

「複数同時に開ける」にしたい場合は、「他のアイテムを閉じる」のブロックを削除してください。

details / summary 要素との比較

HTML 標準の <details> / <summary> 要素を使えば JavaScript なしでアコーディオンが作れます。

HTML(details / summary: JS 不要)
<!-- JavaScript 不要で開閉できる -->
<details>
  <summary>セクション 1</summary>
  <p>セクション 1 の内容です。</p>
</details>

<details>
  <summary>セクション 2</summary>
  <p>セクション 2 の内容です。</p>
</details>
項目 details / summary JS アコーディオン
JavaScript 不要 必要
アニメーション 標準ではなし(CSS で可能だが制約あり) 自由にカスタマイズ可能
排他的開閉(1 つだけ開く) name 属性(Chrome 120+)で対応 JS で制御
アクセシビリティ 標準で対応 ARIA 属性の手動設定が必要
デザインの自由度 低い(ブラウザデフォルトの三角アイコン) 高い(完全カスタマイズ)
シンプルな FAQ なら details / summary がおすすめ
アニメーション不要でデフォルトのデザインで十分なら、details / summary が最もシンプルです。カスタムデザイン、アニメーション、排他的開閉が必要な場合は JavaScript 実装を使ってください。

details / summary と jQuery での実装については「アコーディオン実装方法の完全比較」、CSS のみの実装は「CSS のみでアコーディオンを作る方法」を参照してください。

バリエーション

初期状態で特定のセクションを開いておく

JavaScript
// ページ読み込み時に最初のアイテムを開く
const firstItem = document.querySelector(".accordion-item");
if (firstItem) {
  firstItem.classList.add("is-open");
  firstItem.querySelector(".accordion-body").style.maxHeight =
    firstItem.querySelector(".accordion-body").scrollHeight + "px";
  firstItem.querySelector(".accordion-header").setAttribute("aria-expanded", "true");
}

矢印アイコンの回転アニメーション

CSS(矢印回転)
.accordion-header::after {
  content: "\25BC"; /* ▼ */
  font-size: 12px;
  transition: transform 0.3s ease;
}
.accordion-item.is-open .accordion-header::after {
  transform: rotate(180deg); /* ▲ に回転 */
}

ハッシュ URL で特定のセクションを開く

JavaScript
// URL の #section2 でそのセクションを開く
if (location.hash) {
  const target = document.querySelector(location.hash);
  if (target) {
    const item = target.closest(".accordion-item");
    if (item) {
      item.classList.add("is-open");
      item.querySelector(".accordion-body").style.maxHeight =
        item.querySelector(".accordion-body").scrollHeight + "px";
      target.scrollIntoView({ behavior: "smooth" });
    }
  }
}

アニメーション無効化対応

CSS
@media (prefers-reduced-motion: reduce) {
  .accordion-body {
    transition: none;
  }
  .accordion-header::after {
    transition: none;
  }
}

実務パターン: FAQ ページの実装

HTML(FAQ ページ)
<div class="faq-section">
  <h2>よくある質問</h2>
  <div class="accordion">
    <div class="accordion-item">
      <button class="accordion-header" aria-expanded="false">送料はいくらですか?</button>
      <div class="accordion-body">
        <div class="accordion-content">
          <p>全国一律 550 円です。5,000 円以上のご注文で送料無料になります。</p>
        </div>
      </div>
    </div>
    <div class="accordion-item">
      <button class="accordion-header" aria-expanded="false">返品はできますか?</button>
      <div class="accordion-body">
        <div class="accordion-content">
          <p>商品到着後 7 日以内であれば返品可能です。</p>
        </div>
      </div>
    </div>
  </div>
</div>

よくある質問

Qheight: auto にアニメーションできないのはなぜですか?
ACSS transition は具体的な数値(0 → 200px 等)の間でアニメーションします。auto は具体的な数値ではないため transition が効きません。代わりに max-height0scrollHeight(実際の高さ)に変化させることでアニメーションを実現します。
QjQuery の slideToggle と何が違いますか?
A動作はほぼ同じですが、本記事の実装はjQuery 不要(Vanilla JS のみ)です。jQuery を既に使っているプロジェクトなら slideToggle の方が簡潔に書けます。新規プロジェクトでは jQuery なしの実装を推奨します。jQuery 版は「jQuery アコーディオンの実装完全ガイド」を参照。
Qdetails / summary 要素で十分ではないですか?
Aシンプルな開閉だけなら details / summary で十分です。ただしスムーズなアニメーション排他的開閉(古いブラウザ対応)、カスタムデザインが必要なら JavaScript 実装が適しています。
QscrollHeight が正しい値を返しません
AscrollHeight は要素が非表示(display: none)の場合 0 を返します。本記事の実装は max-height: 0 + overflow: hidden で非表示にしているため、scrollHeight は正しく取得されます。display: none は使わないでください。
Qアコーディオン内の画像が読み込まれると高さがずれます
A画像の読み込みで scrollHeight が変わるためです。画像に widthheight 属性を指定して事前にスペースを確保するか、ResizeObserver で高さの変化を検知して maxHeight を再設定してください。
Qアコーディオンをネスト(入れ子)にできますか?
Aできます。内側のアコーディオンが開閉すると外側の scrollHeight が変わるため、外側の maxHeight も再計算する必要があります。transitionend イベントで親のアコーディオンの maxHeight を更新するか、max-height: 9999px のような大きな値で逃げる方法があります。

まとめ

JavaScript アコーディオンの実装ポイントをまとめます。

ポイント 内容
開閉の仕組み max-height: 0 ⇔ scrollHeight + overflow: hidden
アニメーション CSS transition: max-height 0.3s ease
ヘッダー要素 button 要素を使う(キーボード・スクリーンリーダー対応)
ARIA aria-expanded=”true/false” を開閉時に更新
1 つだけ開くパターン 他の is-open を全て解除してから開く
アイコン CSS ::after + content / transform: rotate で回転
アニメーション無効化 @media (prefers-reduced-motion: reduce)
JS 不要の代替 details / summary 要素(アニメーション不要な場合)

jQuery での実装は「jQuery アコーディオンの実装完全ガイド」、CSS のみの実装は「CSS のみでアコーディオンを作る方法」、実装方法の比較は「アコーディオン実装方法の完全比較」も併せて参照してください。