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