本文へスキップ
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