【Vue.js】シンプルなアコーディオンを作成する方法

アコーディオンは、見出しをクリックすると内容が開閉するUIです。FAQや設定画面など、情報を整理して見せたい場面でよく使われます。この記事では、Vue 3(<script setup>)で、キーボードや読み上げソフトでも操作できる、シンプルなアコーディオンを作ります。

スポンサーリンク

完成コード:同時に1つだけ開くアコーディオン

開いている項目のidを1つだけ持つ方式です。別の項目をクリックすると、前の項目が閉じます。同じ項目をもう一度クリックすると、閉じて全部が閉じた状態になります。

Vue:アコーディオンのコンポーネント
<script setup>
import { ref } from 'vue'

// 開いている項目の id(null なら全部閉じている)
const activeId = ref(null)

const items = [
  { id: 1, title: 'Section 1', content: 'Content for section 1' },
  { id: 2, title: 'Section 2', content: 'Content for section 2' },
  { id: 3, title: 'Section 3', content: 'Content for section 3' }
]

function toggle(id) {
  activeId.value = activeId.value === id ? null : id
}
</script>

<template>
  <div class="accordion">
    <div v-for="item in items" :key="item.id" class="accordion-item">
      <h3 class="accordion-heading">
        <button
          type="button"
          class="accordion-header"
          :id="`header-${item.id}`"
          :aria-expanded="activeId === item.id"
          :aria-controls="`panel-${item.id}`"
          @click="toggle(item.id)"
        >
          {{ item.title }}
        </button>
      </h3>
      <div
        v-show="activeId === item.id"
        :id="`panel-${item.id}`"
        role="region"
        :aria-labelledby="`header-${item.id}`"
        class="accordion-content"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

見た目を整えるスタイルは、次のとおりです。

CSS:スタイル
<style scoped>
.accordion {
  border: 1px solid #ddd;
  border-radius: 4px;
}

.accordion-item {
  border-bottom: 1px solid #ddd;
}

.accordion-item:last-child {
  border-bottom: none;
}

.accordion-heading {
  margin: 0;
}

/* button の既定の見た目を、ヘッダーとして整える */
.accordion-header {
  display: block;
  width: 100%;
  padding: 10px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  background: #f1f1f1;
  border: none;
}

.accordion-header:focus-visible {
  outline: 2px solid #0284c7;
  outline-offset: -2px;
}

.accordion-content {
  padding: 10px;
}
</style>

コードのポイント

ヘッダーは<div>ではなく<button>にする

<div>にクリックイベントを付けただけでは、Tabキーでフォーカスが当たらず、EnterやSpaceでも開閉できません。読み上げソフトにも、開閉できるボタンだと伝わりません。<button type="button">にすれば、キーボード操作が標準で使えます。W3CのWAI-ARIAのアコーディオンの例でも、ヘッダーは見出し要素の中のボタンとされています。

aria-expandedとaria-controlsを付ける

aria-expandedは、パネルが開いていればtrue、閉じていればfalseにします。aria-controlsには、開閉するパネルのidを指定します。上のコードでは、:aria-expanded="activeId === item.id"で、状態と同期しています。

keyにはインデックスではなくidを使う

項目にあらかじめidを持たせ、:key="item.id"にしています。インデックスをkeyにしない理由はv-forで配列やオブジェクトをループして表示する方法で解説しています。

v-showで切り替える

開閉にはv-showを使っています。閉じているときも要素はDOMに残り、displayだけが切り替わります。aria-controlsが指す要素が常に存在し、パネル内の入力欄などの状態も保たれます。v-ifとの違いはv-ifで条件分岐させる方法をご覧ください。

複数の項目を同時に開けるようにする

開いている項目を複数持ちたい場合は、idのSetで管理します。テンプレートは、activeId === item.idの部分をisOpen(item.id)に置き換えるだけです。

Vue:複数の項目を同時に開く
<script setup>
import { ref } from 'vue'

// 開いている項目の id を Set で持つ(複数同時に開ける)
const openIds = ref(new Set())

function toggle(id) {
  if (openIds.value.has(id)) {
    openIds.value.delete(id)
  } else {
    openIds.value.add(id)
  }
}

function isOpen(id) {
  return openIds.value.has(id)
}
</script>

<template>
  <!-- v-show と aria-expanded を isOpen(item.id) に置き換えるだけ -->
  <button
    type="button"
    :aria-expanded="isOpen(item.id)"
    :aria-controls="`panel-${item.id}`"
    @click="toggle(item.id)"
  >
    {{ item.title }}
  </button>
  <div v-show="isOpen(item.id)" :id="`panel-${item.id}`">...</div>
</template>

HTML標準のdetails要素という選択肢

開閉するだけなら、JavaScriptなしでも、HTML標準の<details>と<summary>で作れます。キーボード操作にも標準で対応しています。name属性を同じ値にそろえると、同時に1つだけ開くアコーディオンになります。

HTML:detailsによるアコーディオン
<!-- JavaScript なしで作れる。name を揃えると、同時に1つだけ開く -->
<details name="faq" open>
  <summary>Section 1</summary>
  <p>Content for section 1</p>
</details>
<details name="faq">
  <summary>Section 2</summary>
  <p>Content for section 2</p>
</details>

MDNでは、name属性は「広く使える機能」とされています。対象にするブラウザの対応状況は、公開前にMDNで確認してください。開閉の状態をVueのデータとして扱いたい場合や、見た目を細かく制御したい場合は、この記事のようにVueで作るほうが向いています。

アニメーションを付けたい場合

上のコードは、開閉が一瞬で切り替わります。滑らかに開閉させたい場合は、アニメーション付きアコーディオンを実装する方法で、transitionとrefを使った方法を解説しています。

関連記事

よくある質問(FAQ)

Qヘッダーを<div>ではなく<button>にするのはなぜですか?
Aキーボードで操作でき、読み上げソフトにも開閉できるボタンだと伝わるためです。<div>のクリックイベントだけでは、TabキーでもEnterキーでも操作できません。
Q複数の項目を同時に開けるようにするには?
A開いている項目のidを、Setや配列で持ちます。クリックしたときに、そのidの追加と削除を切り替えます。
Qv-showではなくv-ifでもよいですか?
A動きます。ただし、閉じている間は要素がDOMから消えるため、aria-controlsが指す先がなくなり、パネル内の入力値なども失われます。開閉を繰り返すUIではv-showが向いています。
QJavaScriptなしでアコーディオンは作れますか?
A作れます。<details>と<summary>を使い、name属性をそろえると、同時に1つだけ開く動きになります。

まとめ

アコーディオンは、開いている項目のidを持ち、v-showで切り替えるだけで作れます。ヘッダーを<button>にして、aria-expandedとaria-controlsを付けることで、キーボードや読み上げソフトにも対応できます。複数同時に開く、アニメーションを付ける、<details>で済ませる、といった発展も、目的に合わせて選んでください。