概要
フォームラベルの横に「必須」「任意」バッジを表示し、aria-required を入力欄に付けることでスクリーンリーダーにも必須・任意を伝える。RequiredBadge / OptionalBadge を独立コンポーネントにすることで任意の場所に再利用できる。
インストール
# 追加インストールは不要
実装
バッジコンポーネント
// components/RequiredBadge.tsx
export function RequiredBadge() {
return (
<span
className="ml-1 inline-block rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-600"
aria-hidden="true"
>
必須
</span>
);
}
// components/OptionalBadge.tsx
export function OptionalBadge() {
return (
<span
className="ml-1 inline-block rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-500"
aria-hidden="true"
>
任意
</span>
);
}
フォームフィールドコンポーネント
// components/FormField.tsx
import { RequiredBadge } from "./RequiredBadge";
import { OptionalBadge } from "./OptionalBadge";
type Props = {
id: string;
label: string;
required?: boolean;
children: React.ReactNode;
};
export function FormField({ id, label, required = false, children }: Props) {
return (
<div className="space-y-1">
<label htmlFor={id} className="flex items-center text-sm font-medium text-gray-700">
{label}
{required ? <RequiredBadge /> : <OptionalBadge />}
</label>
{children}
</div>
);
}
入力欄への適用
// components/SampleCreateForm.tsx
"use client";
import { FormField } from "./FormField";
export function SampleCreateForm() {
return (
<form className="space-y-4">
<FormField id="title" label="タイトル" required>
<input
id="title"
type="text"
aria-required="true"
className="w-full rounded border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-400"
/>
</FormField>
<FormField id="summary" label="概要">
<textarea
id="summary"
rows={3}
aria-required="false"
className="w-full rounded border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-400"
/>
</FormField>
<FormField id="category" label="カテゴリ" required>
<select
id="category"
aria-required="true"
className="w-full rounded border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-400"
>
<option value="">選択してください</option>
<option value="routing">ルーティング</option>
<option value="testing">テスト</option>
</select>
</FormField>
<button
type="submit"
className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
>
保存
</button>
</form>
);
}
バッジを props で切り替えるパターン
// components/FieldLabel.tsx — badge を props で制御する代替設計
type BadgeVariant = "required" | "optional" | "none";
type Props = {
htmlFor: string;
children: React.ReactNode;
badge?: BadgeVariant;
};
const BADGE_CONFIG: Record<
Exclude<BadgeVariant, "none">,
{ text: string; className: string }
> = {
required: {
text: "必須",
className: "ml-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-600",
},
optional: {
text: "任意",
className: "ml-1 rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-500",
},
};
export function FieldLabel({ htmlFor, children, badge = "none" }: Props) {
const config = badge !== "none" ? BADGE_CONFIG[badge] : null;
return (
<label htmlFor={htmlFor} className="flex items-center text-sm font-medium text-gray-700">
{children}
{config && (
<span className={config.className} aria-hidden="true">
{config.text}
</span>
)}
</label>
);
}
ポイント
- バッジに
aria-hidden="true"を付けることで、スクリーンリーダーがバッジテキストを読み上げるのを防ぐ。必須かどうかは入力欄のaria-requiredから伝えるため、バッジは視覚的な補助にとどめる aria-required="true"を<input>/<textarea>/<select>に付けることで、スクリーンリーダーがフォーカス時に「必須」と読み上げる。HTML のrequired属性と組み合わせると、ブラウザのネイティブバリデーションも有効になるFormFieldコンポーネントにrequiredprops を渡すだけでラベルのバッジとaria-requiredを一元管理できる。ただしchildrenの入力欄には個別にaria-requiredを設定する必要がある- 必須バッジは赤系(
bg-red-100 text-red-600)、任意バッジはグレー系(bg-gray-100 text-gray-500)にすることで、色覚異常のユーザーにも視覚的な強弱を伝えられる BADGE_CONFIGパターンを使うと、将来「推奨」などのバッジ種別を追加するときにBADGE_CONFIGにエントリを加えるだけで拡張できるhtmlForとidを一致させることで、ラベルクリックで入力欄にフォーカスが移る。スクリーンリーダーも<label>と入力欄を関連付けて読み上げる