UI Library
アコーディオン
よくある質問や補足情報を折りたたむUI。aria-expanded と aria-controls を持ち、キーボードだけで開閉できます。
components/uilib/Accordion.tsx
'use client'
import { useId, useState, type ReactNode } from 'react'
import s from './Accordion.module.scss'
export type AccordionItem = {
/** 見出し(質問) */
question: ReactNode
/** 開いたときの中身 */
answer: ReactNode
}
/**
* アコーディオン。よくある質問や補足情報を折りたたむUI。
*
* a11y:
* - 開閉状態は button の aria-expanded に出す(見た目の記号だけに頼らない)
* - aria-controls でパネルと紐づけ、閉じているあいだは hidden にして
* 読み上げ・Tab移動の対象から外す
* - Tabで移動し Enter / Space で開閉できる(button 要素なので既定で満たす)
*
* `single` を立てると同時に開くのは1つだけになる。
*/
export function Accordion({
items,
single = false,
flush = false,
}: {
items: AccordionItem[]
single?: boolean
/** 上端の罫線を出さない(デモ枠の中で使うとき用) */
flush?: boolean
}) {
const baseId = useId()
const [open, setOpen] = useState<number[]>([])
const toggle = (i: number) => {
setOpen((prev) => {
const isOpen = prev.includes(i)
if (single) return isOpen ? [] : [i]
return isOpen ? prev.filter((n) => n !== i) : [...prev, i]
})
}
return (
<ul className={[s.acc, flush && s.flush].filter(Boolean).join(' ')}>
{items.map((item, i) => {
const panelId = `${baseId}-panel-${i}`
const isOpen = open.includes(i)
return (
<li className={s.item} key={i}>
<button
className={s.btn}
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => toggle(i)}
>
<span className={s.q}>{item.question}</span>
<span className={s.sign} aria-hidden="true" />
</button>
<div className={s.panel} id={panelId} hidden={!isOpen}>
{item.answer}
</div>
</li>
)
})}
</ul>
)
}このページに表示しているのは、実際にこのサイトで動いているソースそのものです。見本用に書き写したコードではありません。スタイルは同じ階層の Accordion.module.scss にあり、色・余白はすべてデザイントークンの CSS カスタムプロパティを参照しています。
States
状態変化もすべて実装済み
押したとき・入力待ちのとき・エラーのとき——実際の運用で必要になる表示は、あとから作り足す必要がないよう最初から含めてお渡しします。
- default
- hover
- focus
- active
- disabled
- loading
- error
- success