UI Library
ハンバーガーメニュー
狭い画面でナビゲーションを畳むUI。閉じているあいだメニューは hidden なので、見えないリンクにフォーカスが入りません。
components/uilib/MenuBurger.tsx
'use client'
import { useEffect, useId, useRef, useState } from 'react'
import s from './MenuBurger.module.scss'
export type MenuLink = { href: string; label: string }
/**
* ハンバーガーメニュー。
*
* a11y:
* - 開閉ボタンは <button>。aria-expanded で状態を、aria-controls で対象を示す
* - 閉じているあいだメニューは hidden。読み上げにも Tab 移動にも出さない
* (display:none にせず視覚だけ隠すと、見えないリンクにフォーカスが入る)
* - Escape で閉じ、フォーカスを開閉ボタンに戻す
* - 開いたら最初のリンクへフォーカスを移す
* - ボタンには文字ラベルを併記する(三本線の記号だけでは意味が伝わらない)
*/
export function MenuBurger({ links }: { links: MenuLink[] }) {
const id = useId()
const [open, setOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLUListElement>(null)
useEffect(() => {
if (!open) return
menuRef.current?.querySelector<HTMLElement>('a')?.focus()
}, [open])
useEffect(() => {
if (!open) return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return
setOpen(false)
buttonRef.current?.focus()
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [open])
return (
<div className={s.menu}>
<button
ref={buttonRef}
className={s.button}
type="button"
aria-expanded={open}
aria-controls={id}
onClick={() => setOpen((v) => !v)}
>
<span className={s.bars} aria-hidden="true">
<span />
<span />
<span />
</span>
{open ? '閉じる' : 'メニュー'}
</button>
<ul className={s.list} id={id} hidden={!open}>
{links.map((link) => (
<li key={link.href}>
<a className={s.link} href={link.href} onClick={() => setOpen(false)}>
{link.label}
</a>
</li>
))}
</ul>
</div>
)
}このページに表示しているのは、実際にこのサイトで動いているソースそのものです。見本用に書き写したコードではありません。スタイルは同じ階層の MenuBurger.module.scss にあり、色・余白はすべてデザイントークンの CSS カスタムプロパティを参照しています。
States
状態変化もすべて実装済み
押したとき・入力待ちのとき・エラーのとき——実際の運用で必要になる表示は、あとから作り足す必要がないよう最初から含めてお渡しします。
- default
- hover
- focus
- active
- disabled
- loading
- error
- success