アコーディオンは、見出しをクリックすると内容が開閉するUIです。FAQや設定画面など、情報を整理して見せたい場面でよく使われます。この記事では、Vue 3(<script setup>)で、キーボードや読み上げソフトでも操作できる、シンプルなアコーディオンを作ります。
完成コード:同時に1つだけ開くアコーディオン
開いている項目のidを1つだけ持つ方式です。別の項目をクリックすると、前の項目が閉じます。同じ項目をもう一度クリックすると、閉じて全部が閉じた状態になります。
<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>
見た目を整えるスタイルは、次のとおりです。
<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)に置き換えるだけです。
<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つだけ開くアコーディオンになります。
<!-- 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を使った方法を解説しています。
関連記事
- ボタンのクリックなどのイベントはv-onでDOMイベントを購読する方法
- 開いている項目の見た目を変えるなら動的クラスとスタイルを切り替える方法
よくある質問(FAQ)
<div>のクリックイベントだけでは、TabキーでもEnterキーでも操作できません。idを、Setや配列で持ちます。クリックしたときに、そのidの追加と削除を切り替えます。aria-controlsが指す先がなくなり、パネル内の入力値なども失われます。開閉を繰り返すUIではv-showが向いています。<details>と<summary>を使い、name属性をそろえると、同時に1つだけ開く動きになります。まとめ
アコーディオンは、開いている項目のidを持ち、v-showで切り替えるだけで作れます。ヘッダーを<button>にして、aria-expandedとaria-controlsを付けることで、キーボードや読み上げソフトにも対応できます。複数同時に開く、アニメーションを付ける、<details>で済ませる、といった発展も、目的に合わせて選んでください。

