フロントエンドアクセシビリティパターン集 は何をしますか?
React と Next.js のための実践的なアクセシビリティパターン集。コードレビューで最も頻繁に指摘される問題(フォームラベルの欠如、誤ったARIA使用法、非セマンティックなインタラクティブ要素、壊れたキーボードナビゲーション)をカバーします。
使用するタイミング
- フォームコンポーネント(
<input>、<select>、<textarea>)を構築・レビューするとき - インタラクティブ要素(モーダル、ドロップダウン、ツールチップ、タブ)を作成するとき
onClickを持つ<div>または<span>を使用するとき- 任意の要素に
aria-*属性を追加するとき - キーボードナビゲーションまたはフォーカス管理を実装するとき
- コードレビューツール(CodeRabbit、ESLint a11y)からアクセシビリティフィードバックを受け取ったとき
- スクリーンリーダーをサポートするコンポーネントを構築するとき
フォームのアクセシビリティ
コードレビューで最もよく指摘される問題は、htmlFor / id のペアリングの欠如と、エラーメッセージが接続されていないことです。
ラベルの接続
// 悪い例: ラベルが入力要素に接続されていない — スクリーンリーダーはそれらを関連付けられない
<label>Email</label>
<input type="email" />
// 良い例: htmlFor が input の id と一致している
<label htmlFor="email">Email</label>
<input id="email" type="email" />
必須フィールド
// 悪い例: 視覚的なアスタリスクのみ — スクリーンリーダーには伝わらない
<label htmlFor="email">Email *</label>
<input id="email" type="email" />
// 良い例: required でブラウザのネイティブ検証を有効化、aria-required でスクリーンリーダーに通知
<label htmlFor="email">
Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />
エラーメッセージ
// 悪い例: エラーテキストは視覚的に存在するが、入力要素にリンクされていない
<input id="email" type="email" />
<span className="error">無効なメールアドレスです</span>
// 良い例: aria-describedby で入力要素をエラーメッセージに接続
// aria-invalid でスクリーンリーダーに無効状態を通知
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid={!!error}
/>
{error && (
<span id="email-error" role="alert">
{error}
</span>
)}
完全なアクセシブルフォーム
interface LoginFormProps {
onSubmit: (email: string, password: string) => void;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newErrors: typeof errors = {};
if (!email) newErrors.email = 'メールアドレスは必須です';
if (!password) newErrors.password = 'パスワードは必須です';
if (Object.keys(newErrors).length) {
setErrors(newErrors);
return;
}
onSubmit(email, password);
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="email">
メールアドレス <span aria-hidden="true">*</span>
</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
aria-required="true"
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={!!errors.email}
autoComplete="email"
/>
{errors.email && (
<span id="email-error" role="alert">
{errors.email}
</span>
)}
</div>
<div>
<label htmlFor="password">
パスワード <span aria-hidden="true">*</span>
</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
aria-required="true"
aria-describedby={errors.password ? 'password-error' : undefined}
aria-invalid={!!errors.password}
autoComplete="current-password"
/>
{errors.password && (
<span id="password-error" role="alert">
{errors.password}
</span>
)}
</div>
<button type="submit">ログイン</button>
</form>
);
}
セマンティックHTML
意図に合った要素を使用してください。スクリーンリーダーとキーボードユーザーはネイティブのセマンティクスに依存しています。
// 悪い例: div にはロール、キーボードサポート、アクセシブル名がない
<div onClick={handleClick}>送信</div>
// 良い例: button はフォーカス可能、Enter/Space でアクティブ化、