UI Library
タブ
同じ場所で表示を切り替えるUI。矢印キーでタブ間を移動でき、Tabキーの移動回数はタブが増えても変わりません(roving tabindex)。
components/uilib/Tabs.tsx
'use client'
import { useId, useRef, useState, type ReactNode } from 'react'
import s from './Tabs.module.scss'
export type TabItem = { label: string; panel: ReactNode }
/**
* タブ。
*
* a11y(WAI-ARIA Authoring Practices の tabs パターン):
* - role="tablist" / "tab" / "tabpanel" と aria-selected・aria-controls
* - roving tabindex: 選択中のタブだけが Tab 移動の対象。タブ間の移動は
* 矢印キーで行う(タブが増えても Tab の回数が増えない)
* - Home / End で先頭・末尾へ
* - パネルは tabIndex={0} にして、中に操作要素がなくてもスクロールできるようにする
*/
export function Tabs({ items }: { items: TabItem[] }) {
const baseId = useId()
const [active, setActive] = useState(0)
const tabRefs = useRef<(HTMLButtonElement | null)[]>([])
const focusTab = (i: number) => {
setActive(i)
tabRefs.current[i]?.focus()
}
const onKeyDown = (e: React.KeyboardEvent) => {
const last = items.length - 1
switch (e.key) {
case 'ArrowRight':
focusTab(active === last ? 0 : active + 1)
break
case 'ArrowLeft':
focusTab(active === 0 ? last : active - 1)
break
case 'Home':
focusTab(0)
break
case 'End':
focusTab(last)
break
default:
return
}
e.preventDefault()
}
return (
<div className={s.tabs}>
<div className={s.list} role="tablist" aria-label="表示の切り替え" onKeyDown={onKeyDown}>
{items.map((item, i) => (
<button
key={item.label}
ref={(el) => {
tabRefs.current[i] = el
}}
className={s.tab}
type="button"
role="tab"
id={`${baseId}-tab-${i}`}
aria-selected={active === i}
aria-controls={`${baseId}-panel-${i}`}
tabIndex={active === i ? 0 : -1}
onClick={() => setActive(i)}
>
{item.label}
</button>
))}
</div>
{items.map((item, i) => (
<div
key={item.label}
className={s.panel}
role="tabpanel"
id={`${baseId}-panel-${i}`}
aria-labelledby={`${baseId}-tab-${i}`}
hidden={active !== i}
tabIndex={0}
>
{item.panel}
</div>
))}
</div>
)
}このページに表示しているのは、実際にこのサイトで動いているソースそのものです。見本用に書き写したコードではありません。スタイルは同じ階層の Tabs.module.scss にあり、色・余白はすべてデザイントークンの CSS カスタムプロパティを参照しています。
States
状態変化もすべて実装済み
押したとき・入力待ちのとき・エラーのとき——実際の運用で必要になる表示は、あとから作り足す必要がないよう最初から含めてお渡しします。
- default
- hover
- focus
- active
- disabled
- loading
- error
- success