アコーディオンUIを作りたいけれど、できればJavaScriptを書かずに済ませたい、と感じることはないでしょうか。

この記事では、JSを1行も書かずにアコーディオンを実装する2つの方法を、コード付きで対比します。<details>/<summary> のネイティブ実装と、<input type="checkbox">:checked を兄弟セレクタで参照する「チェックボックスハック」の2手法です。

それぞれの制約・アクセシビリティの違い・推奨用途まで踏み込み、第一選択を迷わず決められる状態を目指します。実際の動きはデモで確認できます。

🌐 デモ1: <details> 版(GitHub Pages)

🌐 デモ2: チェックボックスハック版(GitHub Pages)

💻 ソースコード1: 002_details-basic(GitHub)

💻 ソースコード2: 003_css-only(GitHub)

この記事で分かること
  • JS不要でアコーディオンを作る2つの方法(<details> / チェックボックスハック)
  • それぞれの実装コード(HTML + CSS)と仕組みの要点
  • アクセシビリティの違いと、迷ったときの第一選択
  • <details> で作るか、チェックボックスハックで作るか」の判断軸

2つの実装方法と選び方の早見

JS不要でアコーディオンを作るときの現実的な選択肢は2つです。違いを早見表で押さえておきます。

観点方法1: <details>方法2: チェックボックスハック
JS必要不要不要
a11y(支援技術)ネイティブで「展開可能」と読み上げ「チェックボックス」と読み上げ
自由なスタイリングデフォルトマーカー除去にひと手間自由度が高い(<label> ベース)
兄弟要素との連動details[open] の波及範囲に制約:checked ~ で柔軟に組める
第一選択これを最初に検討<details> で叶わない要件のときの代替

迷ったら <details> から検討するのが穏当です。チェックボックスハックは「<details> の標準動作では届かない見た目や、兄弟要素との連動が必要になったとき」の代替手段として持っておくと、選択がぶれにくくなります。

a11yの行が示すとおり、両者はスクリーンリーダーへの伝わり方が違います。アクセシビリティ要件があるなら方法1、見た目の自由度を優先するなら方法2、という棲み分けで考えるとシンプルです。

方法1: <details> で作る(推奨)

標準HTML要素 <details>/<summary> を使う方法です。仕様として「折りたたみ要素」が用意されているため、JSもARIA属性も書かずにアコーディオンUIが完成します。コピペで動作する完成形を先に提示します。

HTML
<div class="c-accordion">
  <!-- 項目 1 -->
  <details class="c-accordion__item">
    <summary class="c-accordion__header">
      <span class="c-accordion__title"><code><details></code> 要素とは何か?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </summary>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <code><details></code> は HTML5 で標準化された折りたたみ要素です。
          最初の子要素として <code><summary></code> を置き、それ以降の子要素が開閉される本文になります。
        </p>
        <p>
          ブラウザが開閉トグルをネイティブで処理するため、JavaScript を1行も書かずにアコーディオン UI を構築できます。
        </p>
      </div>
    </div>
  </details>

  <!-- 項目 2 -->
  <details class="c-accordion__item">
    <summary class="c-accordion__header">
      <span class="c-accordion__title"><code>open</code> 属性の使い方は?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </summary>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <code><details open></code> のように <code>open</code> 属性を付けると、初期状態で本文が開いた状態になります。
          属性を外せば閉じた状態が初期表示です。
        </p>
        <p>
          ユーザーが開閉操作するたびにブラウザが <code>open</code> 属性を自動で付け外しするため、
          CSS では <code>details[open]</code> セレクタで「開いている状態」のスタイルを書けます。
        </p>
      </div>
    </div>
  </details>

  <!-- 項目 3 -->
  <details class="c-accordion__item">
    <summary class="c-accordion__header">
      <span class="c-accordion__title">JS 不要実装の利点と限界は?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </summary>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <strong>利点</strong>: JS を読み込まずに動作するため軽量で、JS が落ちた場合も問題なく機能します。
          キーボード操作・スクリーンリーダー対応もブラウザが標準で処理してくれます。
        </p>
        <p>
          <strong>限界</strong>: 高さアニメーションは標準ではサポートされず、
          「1つだけ開く(排他制御)」のような独自挙動には JS が必要になります。
          シンプルな開閉で十分な場面に向いています。
        </p>
      </div>
    </div>
  </details>

  <!-- 項目 4 -->
  <details class="c-accordion__item">
    <summary class="c-accordion__header">
      <span class="c-accordion__title">アクセシビリティはどう担保される?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </summary>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <code><summary></code> はネイティブにフォーカス可能な要素として扱われ、
          Tab キーでフォーカス移動・Enter / Space キーで開閉という挙動をブラウザが処理します。
          そのため <code>tabindex</code><code>aria-expanded</code> を自前で付与する必要はありません。
        </p>
        <p>
          スクリーンリーダーには「展開可能/折りたたみ可能」という状態が標準で伝わります。
          シンプルな実装で WAI-ARIA 相当の支援技術対応が得られるのが <code><details></code> の最大の強みです。
        </p>
      </div>
    </div>
  </details>
</div>
OPEN
CSS
/* ================================================
   カスタムプロパティ
   001_js-toggle と同一のトークン定義を再掲する。
   シリーズ全 snippet で色・サイズを揃えるための共有
   トークンであり、各 snippet がスタンドアロンで動く
   ようにここで一元定義する(値は改変しない)。
   ================================================ */
:root {
  --accordion-color-text:        #2b2b2b;
  --accordion-color-text-muted:  #555;
  --accordion-color-border:      #e0e0e0;
  --accordion-color-bg:          #ffffff;
  --accordion-color-bg-hover:    #f5f7fa;
  --accordion-color-bg-active:   #eef3fa;
  --accordion-color-accent:      #0066cc;
  --accordion-color-focus-ring:  #0066cc;

  --accordion-radius:            8px;
  --accordion-header-padding-y:  16px;
  --accordion-header-padding-x:  20px;
  --accordion-body-padding-y:    16px;
  --accordion-body-padding-x:    20px;

  --accordion-transition:        0.2s ease;
}

/* ================================================
   ベースリセット(デモページ用)
   ================================================ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
    "Hiragino Sans", "Noto Sans JP", sans-serif;
  color: var(--accordion-color-text);
  background-color: #fafafa;
  line-height: 1.7;
}

code {
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  font-size: 0.92em;
  padding: 0.1em 0.35em;
  background-color: #eef0f3;
  border-radius: 4px;
}

/* ================================================
   l-main / p-section: デモページのレイアウト
   ================================================ */
.l-main {
  max-width: 720px;
  margin-inline: auto;
  padding: 40px 20px;
}

.p-section__title {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 16px;
}

.p-section__text {
  margin: 0 0 24px;
  color: var(--accordion-color-text-muted);
}

@media (min-width: 768px) {
  .p-section__title {
    font-size: 28px;
  }
}

/* ================================================
   c-accordion: アコーディオン本体
   001 と同一のコンテナスタイル
   ================================================ */
.c-accordion {
  border: 1px solid var(--accordion-color-border);
  border-radius: var(--accordion-radius);
  background-color: var(--accordion-color-bg);
  overflow: hidden; /* 角丸を子要素にも適用するため */
}

/* 項目間の区切り線
   <details> 自体に .c-accordion__item を付与しているため
   001 と同じセレクタ(隣接 __item)で区切り線が引ける */
.c-accordion__item + .c-accordion__item {
  border-top: 1px solid var(--accordion-color-border);
}

/* ================================================
   c-accordion__header: <summary> の見た目
   001 では <button> だったが、002 では <summary> に変わる。
   要素は変わるがクラス名は揃え、シリーズ間で BEM 構造を
   一貫させる。
   ================================================ */

/* デフォルトマーカー(▶︎)の除去
   - Chromium / Firefox: list-style: none で消える
   - Safari (WebKit): summary::-webkit-details-marker で消す
   両方の指定が必要(片方だけでは片側で残る)
   ================================================ */
.c-accordion__header {
  /* マーカー除去(Chromium / Firefox) */
  list-style: none;

  /* レイアウト
     <summary> はデフォルト display: list-item だが、
     レイアウト統一のため display: flex に変更する */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  min-height: 44px; /* タップ領域の最低保証(モバイルアクセシビリティ)。
                       padding を変更してもこの最低高さは崩れない。 */
  padding: var(--accordion-header-padding-y) var(--accordion-header-padding-x);

  /* テキスト */
  text-align: left;
  font-size: 16px;
  font-weight: 600;
  color: inherit;

  /* インタラクション */
  cursor: pointer;

  /* 状態遷移
     ヘッダー本体は色変化のみ瞬時切替で良いが、001 と挙動を
     合わせるため background-color と color をトランジション対象に含める。
     高さアニメーションはこの snippet では入れない(snippet 006 animation で対応)。 */
  background-color: var(--accordion-color-bg);
  transition:
    background-color var(--accordion-transition),
    color var(--accordion-transition);
}

/* Safari (WebKit) のマーカー除去
   list-style: none では消えないため、別ルールで擬似要素を非表示化 */
.c-accordion__header::-webkit-details-marker {
  display: none;
}

/* マウス系デバイスかつホバー操作が可能な場合のみ適用
   001 と同じ media query で揃える */
@media (any-hover: hover) and (pointer: fine) {
  .c-accordion__header:hover {
    background-color: var(--accordion-color-bg-hover);
  }
}

/* キーボードフォーカス時のフォーカスリング
   <summary> はネイティブでフォーカス可能なため、追加属性なしで
   :focus-visible が機能する。001 と同一のリングデザイン。 */
.c-accordion__header:focus-visible {
  outline: 2px solid var(--accordion-color-focus-ring);
  outline-offset: 2px;
}

/* 開いている項目のヘッダーは背景色とアクセントカラーで強調
   001 では [aria-expanded="true"] セレクタだったが、
   002 ではブラウザが <details> に open 属性を自動付与するため
   details[open] > summary セレクタで同等の状態表現ができる。 */
.c-accordion__item[open] > .c-accordion__header {
  background-color: var(--accordion-color-bg-active);
  color: var(--accordion-color-accent);
}

/* ================================================
   c-accordion__title: ヘッダー内のタイトルテキスト
   001 と同一
   ================================================ */
.c-accordion__title {
  flex: 1;
  min-width: 0; /* テキストが長い場合に折り返せるようにする */
}

/* ================================================
   c-accordion__icon: 開閉インジケータ(▼)
   001 と同じシェブロンを CSS のみで描画。
   open 属性の有無に応じて回転させ開閉状態を視覚化する。
   ================================================ */
.c-accordion__icon {
  flex-shrink: 0;
  display: inline-block;
  width: 12px;
  height: 12px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);            /* 閉じている状態: ▼ */
  transform-origin: center;
  transition: transform var(--accordion-transition);
}

.c-accordion__item[open] > .c-accordion__header .c-accordion__icon {
  transform: rotate(-135deg);          /* 開いている状態: ▲ */
}

/* ================================================
   c-accordion__body: 本文パネル
   001 では hidden 属性で開閉していたが、002 では <details> が
   標準で本文の表示/非表示を制御するため、CSS 側ではスタイリング
   のみ行う。
   ================================================ */
.c-accordion__body {
  background-color: var(--accordion-color-bg);
}

/* ================================================
   c-accordion__content: 本文の余白を持つ内側ラッパー
   001 と同一の2層構造を維持し、シリーズ間の BEM 整合を保つ。
   ================================================ */
.c-accordion__content {
  padding: var(--accordion-body-padding-y) var(--accordion-body-padding-x);
  color: var(--accordion-color-text);
  border-top: 1px solid var(--accordion-color-border);
}

.c-accordion__content > :first-child {
  margin-top: 0;
}

.c-accordion__content > :last-child {
  margin-bottom: 0;
}

.c-accordion__content p {
  margin: 0 0 12px;
}

.c-accordion__content p:last-child {
  margin-bottom: 0;
}

/* ================================================
   prefers-reduced-motion: 動きを減らす設定への配慮
   アイコン回転トランジションを無効化する。
   高さアニメーションはこの snippet では実装していないため
   対象外(snippet 006 animation で本格対応)。
   001 と同一の指定。
   ================================================ */
@media (prefers-reduced-motion: reduce) {
  .c-accordion__header,
  .c-accordion__icon {
    transition: none;
  }
}
OPEN

構造の要点は次の3点です。

  • <details> の最初の子要素として <summary> を置き、それ以降の子要素が開閉される本文になる
  • open 属性を付ければ初期表示で開いた状態にできる(属性はブラウザがクリックのたびに自動で付け外しする)
  • ヘッダーは <summary>、本文は __body / __content の2層構造で、シリーズの他スニペットと同じBEM命名で揃えている

開閉トグル・キーボード操作・支援技術への通知を、ブラウザが標準で処理してくれます。スニペット内のコメントが「なぜそう書いたか」を細かく説明しているので、各小節では要点だけ補足します。

なぜ <details> が第一選択なのか

JSなしで開閉したい場面で <details> を最初に検討すべき理由は次の3つです。

  • JSが一切不要で軽量。スクリプト読み込みエラーに引きずられない
  • 支援技術に「展開可能/折りたたみ可能」がネイティブで伝わる(自前のARIA属性が不要)
  • キーボード操作(Tabでフォーカス → Enter/Spaceで開閉)もブラウザが標準で処理する

「実装が短い」「フォールバックが堅い」「a11yがネイティブ」の3拍子が揃うのが <details> の強みです。

デフォルトマーカー除去の落とし穴(クロスブラウザ対応)

<details> を素のまま使うと <summary> の左に開閉マーカー(▶︎)が出ます。これを消すには ブラウザごとに別ルールを書く必要がある という落とし穴があります。

  • Chromium / Firefox: list-style: none で消える
  • Safari (WebKit): summary::-webkit-details-marker { display: none } が別途必要

片方だけ書くと、もう一方でマーカーが残ります。両方をセットで指定するのが安全です。

開状態のスタイリング(details[open] セレクタ)

<details> は開閉のたびにブラウザが open 属性を自動で付け外しします。これを利用すると details[open] セレクタで「開いている状態」のスタイルをCSSだけで書けます。

スニペットの .c-accordion__item[open] > .c-accordion__header がその実例です。背景色とアクセントカラーを切り替えるだけで、JSによる状態管理を一切書かずに開閉の見た目を表現できます。インジケータ(▼)の回転も同じ仕組みで、開時に rotate(-135deg) を当てて ▲ に回しています。

<details> の限界

<details> で全ての要件をカバーできるわけではありません。標準仕様の枠内では次のような制約があります。

  • 高さアニメーション(開閉時の伸縮)は標準ではサポートされない
  • 「1つだけ開いて他は閉じる」排他制御は単独では作れない(JSが必要)

天井に当たったときの選択肢は2つです。(1) JS実装に切り替えてフル制御する、(2) チェックボックスハックでスタイリング自由度を取りに行く。次の章では (2) を見ていきます。

方法2: チェックボックスハックで作る

もう一つが、<input type="checkbox">:checked 状態を兄弟セレクタ(~)で参照する「チェックボックスハック」です。HTMLの仕組み(<label for> でラベルとチェックボックスを関連付ける)とCSSの兄弟セレクタを組み合わせ、JSなしで開閉を成立させます。こちらもコピペで動作する完成形を先に提示します。

HTML
<div class="c-accordion">
  <!-- 項目 1 -->
  <div class="c-accordion__item">
    <input
      type="checkbox"
      id="c-accordion-toggle-1"
      class="c-accordion__toggle"
    />
    <label for="c-accordion-toggle-1" class="c-accordion__header">
      <span class="c-accordion__title">チェックボックスハックとは何か?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </label>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <code><input type="checkbox"></code><code>:checked</code> 状態を兄弟セレクタ(<code>~</code><code>+</code>)で参照し、
          別の要素のスタイルを切り替えるテクニックです。
          <code><label></code> をチェックボックスに <code>for</code> 属性で関連付ければ、ラベルのクリックでチェック状態が反転するため、
          これを開閉トリガーとして使えます。
        </p>
        <p>
          JavaScript を読み込まずにアコーディオンの開閉が成立するため、JS が無効化された環境や JS の読み込みに失敗した状況でも動作します。
        </p>
      </div>
    </div>
  </div>

  <!-- 項目 2 -->
  <div class="c-accordion__item">
    <input
      type="checkbox"
      id="c-accordion-toggle-2"
      class="c-accordion__toggle"
    />
    <label for="c-accordion-toggle-2" class="c-accordion__header">
      <span class="c-accordion__title">アクセシビリティ上の注意点は?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </label>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          実体は <code><input type="checkbox"></code> なので、スクリーンリーダーには「チェックボックス」として読み上げられます。
          「展開可能なボタン」とは伝わらない点が、本来のアコーディオン UI からの逸脱です。
        </p>
        <p>
          また、チェックボックス本体を <code>display: none</code> で隠すと Tab フォーカスも失われて操作不能になります。
          本スニペットでは <code>position: absolute</code> + <code>clip</code> による sr-only パターンで視覚的にだけ隠し、フォーカス可能性を維持しています。
        </p>
      </div>
    </div>
  </div>

  <!-- 項目 3 -->
  <div class="c-accordion__item">
    <input
      type="checkbox"
      id="c-accordion-toggle-3"
      class="c-accordion__toggle"
    />
    <label for="c-accordion-toggle-3" class="c-accordion__header">
      <span class="c-accordion__title"><code><details></code> との違いと使い分けは?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </label>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          <code><details></code> はブラウザに「折りたたみ要素」として認識され、支援技術にも展開状態が標準で伝わります。
          JS なしで開閉したい場合の<strong>第一選択は <code><details></code></strong> です。
        </p>
        <p>
          チェックボックスハックは、<code><details></code> 標準のマーカー / 開閉アニメーション制限を超えて
          「自由なスタイリング・兄弟要素との組み合わせ」を行いたい時の代替手段として位置づけるのが穏当です。
          代償としてセマンティクスが弱くなる点は把握しておく必要があります。
        </p>
      </div>
    </div>
  </div>

  <!-- 項目 4 -->
  <div class="c-accordion__item">
    <input
      type="checkbox"
      id="c-accordion-toggle-4"
      class="c-accordion__toggle"
    />
    <label for="c-accordion-toggle-4" class="c-accordion__header">
      <span class="c-accordion__title">複数同時に開ける?1つずつしか開けない?</span>
      <span class="c-accordion__icon" aria-hidden="true"></span>
    </label>
    <div class="c-accordion__body">
      <div class="c-accordion__content">
        <p>
          各項目が独立した <code><input type="checkbox"></code> を持つため、<strong>複数同時に開ける</strong>挙動になります。
          チェックボックスは互いに状態を干渉せず、それぞれ個別に ON/OFF できるためです。
        </p>
        <p>
          「1つだけ開いて他は閉じる」排他制御を CSS のみで実装したい場合は、
          チェックボックスではなく <code><input type="radio"></code> を同一 <code>name</code> でグルーピングする方法があります。
          ただし「同じラジオを再クリックしても閉じない」など挙動上のクセがあるため、別スニペットで扱います。
        </p>
      </div>
    </div>
  </div>
</div>
OPEN
CSS
/* ================================================
   カスタムプロパティ
   001_js-toggle / 002_details-basic と同一のトークン定義を再掲する。
   シリーズ全 snippet で色・サイズを揃えるための共有
   トークンであり、各 snippet がスタンドアロンで動く
   ようにここで一元定義する(値は改変しない)。
   ================================================ */
:root {
  --accordion-color-text:        #2b2b2b;
  --accordion-color-text-muted:  #555;
  --accordion-color-border:      #e0e0e0;
  --accordion-color-bg:          #ffffff;
  --accordion-color-bg-hover:    #f5f7fa;
  --accordion-color-bg-active:   #eef3fa;
  --accordion-color-accent:      #0066cc;
  --accordion-color-focus-ring:  #0066cc;

  --accordion-radius:            8px;
  --accordion-header-padding-y:  16px;
  --accordion-header-padding-x:  20px;
  --accordion-body-padding-y:    16px;
  --accordion-body-padding-x:    20px;

  --accordion-transition:        0.2s ease;
}

/* ================================================
   ベースリセット(デモページ用)
   ================================================ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
    "Hiragino Sans", "Noto Sans JP", sans-serif;
  color: var(--accordion-color-text);
  background-color: #fafafa;
  line-height: 1.7;
}

code {
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  font-size: 0.92em;
  padding: 0.1em 0.35em;
  background-color: #eef0f3;
  border-radius: 4px;
}

/* ================================================
   l-main / p-section: デモページのレイアウト
   ================================================ */
.l-main {
  max-width: 720px;
  margin-inline: auto;
  padding: 40px 20px;
}

.p-section__title {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 16px;
}

.p-section__text {
  margin: 0 0 24px;
  color: var(--accordion-color-text-muted);
}

@media (min-width: 768px) {
  .p-section__title {
    font-size: 28px;
  }
}

/* ================================================
   c-accordion: アコーディオン本体
   001 / 002 と同一のコンテナスタイル
   ================================================ */
.c-accordion {
  border: 1px solid var(--accordion-color-border);
  border-radius: var(--accordion-radius);
  background-color: var(--accordion-color-bg);
  overflow: hidden; /* 角丸を子要素にも適用するため */
}

/* 項目間の区切り線 */
.c-accordion__item + .c-accordion__item {
  border-top: 1px solid var(--accordion-color-border);
}

/* ================================================
   c-accordion__toggle: チェックボックス本体(003 で新登場)
   sr-only パターンで視覚的にだけ隠す。
   - display: none を使うと Tab フォーカスが失われ a11y が退化する
   - position: absolute + clip で「画面に見えないが
     フォーカスは受け付ける」状態を作る
   フォーカス時は ~ セレクタで .c-accordion__header 側に
   フォーカスリングを表示することで、キーボード操作時に
   フォーカス位置がわかるようにする。
   ================================================ */
.c-accordion__toggle {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ================================================
   c-accordion__header: <label> 要素(クリック対象)
   001 では <button>、002 では <summary> だったが、
   003 では <label for> でチェックボックスに関連付けた
   <label> がクリック対象になる。
   要素は変わるがクラス名は揃え、シリーズ間で BEM 構造を
   一貫させる。
   ================================================ */
.c-accordion__header {
  /* レイアウト */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  min-height: 44px; /* タップ領域の最低保証(モバイルアクセシビリティ)。
                       padding を変更してもこの最低高さは崩れない。 */
  padding: var(--accordion-header-padding-y) var(--accordion-header-padding-x);

  /* テキスト */
  text-align: left;
  font-size: 16px;
  font-weight: 600;
  color: inherit;

  /* インタラクション
     <label> は for で関連付いたチェックボックスの状態を
     クリックで切り替える。CSS としてはカーソルだけ手の形にする。 */
  cursor: pointer;
  user-select: none; /* ラベル連打時のテキスト選択を抑制 */

  /* 状態遷移
     001 / 002 と挙動を合わせ、background-color と color を
     トランジション対象に含める。
     高さアニメーションはこの snippet では入れない(snippet 006 animation で対応)。 */
  background-color: var(--accordion-color-bg);
  transition:
    background-color var(--accordion-transition),
    color var(--accordion-transition);
}

/* マウス系デバイスかつホバー操作が可能な場合のみ適用
   001 / 002 と同じ media query で揃える */
@media (any-hover: hover) and (pointer: fine) {
  .c-accordion__header:hover {
    background-color: var(--accordion-color-bg-hover);
  }
}

/* キーボードフォーカス時のフォーカスリング
   フォーカスを受けるのは sr-only で隠した <input> 側。
   見た目は隣接する <label>(= __header)にリングを表示することで、
   キーボード操作時のフォーカス位置がわかるようにする。
   ~(一般兄弟)セレクタを使うのは、HTML 構造上 __toggle と
   __header が直接の隣接(+)関係でない可能性に備えた汎用形。 */
.c-accordion__toggle:focus-visible ~ .c-accordion__header {
  outline: 2px solid var(--accordion-color-focus-ring);
  outline-offset: 2px;
}

/* 開いている項目のヘッダーは背景色とアクセントカラーで強調
   001 では [aria-expanded="true"]、002 では [open] セレクタだったが、
   003 では :checked 兄弟セレクタで同等の状態表現を行う。
   :has() を使わず兄弟セレクタ(~)に留めることで、互換性を最大化する。 */
.c-accordion__toggle:checked ~ .c-accordion__header {
  background-color: var(--accordion-color-bg-active);
  color: var(--accordion-color-accent);
}

/* ================================================
   c-accordion__title: ヘッダー内のタイトルテキスト
   001 / 002 と同一
   ================================================ */
.c-accordion__title {
  flex: 1;
  min-width: 0; /* テキストが長い場合に折り返せるようにする */
}

/* ================================================
   c-accordion__icon: 開閉インジケータ(▼)
   001 / 002 と同じシェブロンを CSS のみで描画。
   :checked の有無に応じて回転させ開閉状態を視覚化する。
   ================================================ */
.c-accordion__icon {
  flex-shrink: 0;
  display: inline-block;
  width: 12px;
  height: 12px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);            /* 閉じている状態: ▼ */
  transform-origin: center;
  transition: transform var(--accordion-transition);
}

.c-accordion__toggle:checked ~ .c-accordion__header .c-accordion__icon {
  transform: rotate(-135deg);          /* 開いている状態: ▲ */
}

/* ================================================
   c-accordion__body: 本文パネル
   003 では :checked による表示/非表示を CSS で行う。
   - デフォルトは display: none で非表示
   - :checked 時に display: block で表示
   <input> 側に hidden 属性は付けない(display:none 同等になり
   フォーカス不能になるため)。
   ================================================ */
.c-accordion__body {
  display: none;
  background-color: var(--accordion-color-bg);
}

.c-accordion__toggle:checked ~ .c-accordion__body {
  display: block;
}

/* ================================================
   c-accordion__content: 本文の余白を持つ内側ラッパー
   001 / 002 と同一の2層構造を維持し、シリーズ間の BEM 整合を保つ。
   ================================================ */
.c-accordion__content {
  padding: var(--accordion-body-padding-y) var(--accordion-body-padding-x);
  color: var(--accordion-color-text);
  border-top: 1px solid var(--accordion-color-border);
}

.c-accordion__content > :first-child {
  margin-top: 0;
}

.c-accordion__content > :last-child {
  margin-bottom: 0;
}

.c-accordion__content p {
  margin: 0 0 12px;
}

.c-accordion__content p:last-child {
  margin-bottom: 0;
}

/* ================================================
   prefers-reduced-motion: 動きを減らす設定への配慮
   アイコン回転トランジションを無効化する。
   高さアニメーションはこの snippet では実装していないため
   対象外(snippet 006 animation で本格対応)。
   001 / 002 と同等の指定(003 では __header 側のトランジションは
   color/background のみで動きが弱いため、__icon を主対象とする)。
   ================================================ */
@media (prefers-reduced-motion: reduce) {
  .c-accordion__header,
  .c-accordion__icon {
    transition: none;
  }
}
OPEN

仕組みの要点を整理しておきます。

  • <input type="checkbox">:checked 状態を、兄弟セレクタ(~)で他の要素のスタイルに伝える
  • <label for="..."> でチェックボックスに関連付け、ラベルクリックでチェックを反転 → 開閉トリガーとして機能する
  • 同一ページに複数配置するときは、<input>id<label>forインスタンスごとに一意化 する必要がある(衝突するとラベルクリックがどのチェックボックスを切り替えるか曖昧になる)

「折りたたみ要素」として標準化された <details> と違い、フォーム要素(チェックボックス)を流用してUIを組む形になります。意味論的には少し回り道なので、用途を見極めて使う姿勢が大切です。

チェックボックスを sr-only で隠す理由

チェックボックスをそのまま画面に出すと不自然な見た目になります。一方で display: none で隠すと、Tabフォーカスも失われてキーボード操作ができなくなります。

そこで使われるのが position: absolute + clipsr-only パターン です。上のCSSの .c-accordion__toggle セレクタがその実装で、width: 1px; height: 1px; clip: rect(0,0,0,0) 等を組み合わせて視覚的にだけ消しています。視覚は見えなくなりますが、フォーカスは受け付けるため、キーボード操作の動線が保たれます。

フォーカスリングは <label>(= ヘッダー)側に出します。.c-accordion__toggle:focus-visible ~ .c-accordion__header の兄弟セレクタで隣のラベルにoutlineを当てる仕組みで、キーボード操作時のフォーカス位置が視認できます。

開閉状態のスタイリング(:checked ~ セレクタ)

開閉状態は :checked ~ 兄弟セレクタで表現します。<details>details[open] に相当する役割で、CSSだけで「開いた見た目」を切り替えられます。上のCSSの .c-accordion__toggle:checked ~ .c-accordion__header 行と .c-accordion__toggle:checked ~ .c-accordion__body 行がその実装です。

本文パネル(.c-accordion__body)はデフォルトで display: none、チェック時に display: block に切り替えます。:has() を使わず兄弟セレクタ(~)に留めることで、互換性の高い書き方になっています。

アクセシビリティ上のトレードオフ(正直に書く)

ここは見落としがちですが、本記事の核として正直に書きます。チェックボックスハックの実体は <input type="checkbox"> なので、スクリーンリーダーには 「チェックボックス」として読み上げられます

<details> のように「展開可能」「折りたたみ可能」とは伝わりません。本来のアコーディオンUIからは意味論的に少し外れる点を、選択前に把握しておく必要があります。

a11yを重視する要件があるなら、迷わず方法1を選ぶのが穏当です。「方法2は劣っている」というわけではなく、用途しだいで適切な選択になります。

チェックボックスハックの本来の使いどころ

それでもチェックボックスハックを採用する価値があるのは、次のような場面です。

  • <details> 標準のマーカーや開閉動作の制約を超えて 完全に自由なスタイリング をしたい
  • 兄弟要素との連動(:checked ~ .c-accordion__body のような関係)を CSS構造として組み立てたい
  • JSをロードしない静的サイト・軽量実装で、見た目要件が <details> の標準動作で満たせない

セマンティクスが弱くなる代わりに、見た目の自由度を取りに行くテクニック、というのが妥当な位置づけです。

2つを比べてどう選ぶか

ここまでを踏まえて選択の流れを整理します。上から順に判断していくと迷いにくくなります。

  • a11y を最優先するか? → Yes なら方法1(<details>
  • <details> のスタイリング制約で要件が満たせるか? → Yes なら方法1
  • 見た目の自由度や兄弟要素との連動が必要か? → Yes なら方法2(チェックボックスハック)
  • どちらでも満たせる場合 → 方法1(a11y がネイティブ・コード量が少ない・保守性が高い)

迷ったら <details>、というのが結論です。標準HTML要素として用意された仕組みに乗ることで、実装コスト・フォールバック耐性・a11yを同時に取れるためです。

<details> の天井に当たったら、JS実装に切り替えるかチェックボックスハックで自由度を取りに行くかの2択を検討する流れです。JS実装が必要なケースは記事末尾の関連記事ブロックから参照してください。

よくある質問

<details> で高さアニメーションを付けたいです。

<details> 標準の開閉では高さトランジションが効きません。CSSの interpolate-size: allow-keywordsgrid-template-rows: 0fr → 1fr 系のテクニックを別途追加すれば実装可能ですが、難易度が一段上がります。シンプルな開閉で十分な場面では標準のまま使うのが穏当です。

チェックボックスハックで排他制御(1つだけ開く)はできますか?

<input type="checkbox"> ではなく、同一 name でグルーピングした <input type="radio"> を使えば、CSSのみで排他制御に近い挙動を作れます。ただし「同じラジオを再クリックしても閉じない」など挙動のクセがあるため、JS実装のほうが直感的な場面も多いです。

SEO的にはどちらが有利ですか?

アコーディオンの内側にあるテキストは、いずれの手法でもHTML上に存在するためクロールされます。<details> は折りたたまれていてもインデックスされる点をGoogleが公式にアナウンス済みで、チェックボックスハックでも同様です。SEO的な優劣はほぼなく、選択はa11yとスタイリング自由度で判断するのが妥当です。

同一ページに複数のチェックボックスハックを置くときの注意点は?

<input>id<label>forインスタンスごとに一意化 してください。c-accordion-toggle-faq-1 のように用途プレフィックスを付けると衝突を防ぎやすくなります。<details> ではこの種のid重複問題が起きないため、運用負担が小さい点も第一選択の理由です。

まとめ

JavaScriptを書かずにアコーディオンを作る2つの方法を見てきました。ポイントを整理します。

  • JS不要のアコーディオンは <details> とチェックボックスハックの2択
  • 第一選択は <details>(a11yがネイティブ・コード量が少ない・運用が穏当)
  • チェックボックスハックは「自由なスタイリング・兄弟要素との連動」が必要なときの代替手段
  • 方法2はスクリーンリーダーに「チェックボックス」と読み上げられる点を把握しておく
  • どちらもJSなしで動くため、JSが落ちた環境でも安全に機能する

迷ったら <details> から始め、要件が天井に当たったらチェックボックスハックかJS実装に切り替える、という順序で検討するとシンプルです。

【関連記事】
(公開後に有効化)
→ HTML・CSSの次は何を学ぶ?フロントエンド学習ロードマップの組み立て方(公開済)
→ FLOCSSとは?基本の考え方と実際の書き方を分かりやすく解説(公開済)
→ jQueryからバニラJSへの書き換えパターン11選|$(...) を手放すときに迷わない対応表(公開済)

【シリーズ:アコーディオンUIシリーズ】
→ アコーディオン実装パターンまとめ(記事000・公開予定)
→ バニラJSで作るアコーディオン|aria-expanded で実装するARIA対応の基本形(記事001・公開済)
→ アコーディオンの開閉制御パターン|複数同時 vs 排他制御(記事003・公開予定)
→ 高さアニメーション付きアコーディオン(記事004・公開予定)
→ 入れ子アコーディオン(記事005・公開予定)