フォーム
フォームは情報を「入力させる」場所。a11y の問題が露骨に出やすい。ラベル付け、エラー伝達、 適切な input type、この3つが押さえられているかで品質が決まる。
すべての入力に <label>
スクリーンリーダはフォーカスが入った瞬間に「メールアドレス、必須、テキスト入力」のように
ラベル + 状態 + role を読む。<label> がないと「テキスト入力」しか言われない。
付け方は2通り
<label for="email">メールアドレス</label>
<input id="email" type="email" name="email" />
<label>
メールアドレス
<input type="email" name="email" />
</label>
どちらでも機能する。A を推奨するのは、CSS でレイアウト柔軟性が高いから。 ラベルのクリックでフォーカスが入るのも両方とも同じ。
placeholder はラベルの代わりにならない
最頻出の間違い。placeholder は例の入力値を示すもので、ラベルではない。
- 入力中は消えるので「何の欄だったか」が分からなくなる
- コントラストが弱く視認性が低い(多くのブラウザで薄いグレー)
- スクリーンリーダの読み上げが UA・ブラウザで揺れる
- フォーム自動入力がうまく動かないケースがある
<!-- BAD -->
<input type="email" placeholder="メールアドレス" />
<!-- GOOD -->
<label for="email">メールアドレス</label>
<input id="email" type="email" placeholder="user@example.com" />
ラジオ・チェックボックスは <fieldset> + <legend>
複数の選択肢が「1つの問い」に紐づく時、共通の見出しが要る。それが <legend>。
<fieldset>
<legend>配送方法</legend>
<input id="ship-std" type="radio" name="ship" value="std" />
<label for="ship-std">通常配送</label>
<input id="ship-express" type="radio" name="ship" value="exp" />
<label for="ship-express">速達</label>
</fieldset>
スクリーンリーダは「配送方法、通常配送、ラジオボタン、選択されていません」のように legend を含めて読み上げる。
適切な input type を使う
モバイルでは type に応じてキーボードが切り替わる。a11y であると同時に UX 改善。
| type | 用途 | キーボード |
|---|---|---|
text | 汎用テキスト | 標準 |
email | メールアドレス | @ や . が出やすい |
tel | 電話番号 | 数字キーパッド |
number | 数値(数学的) | 数字 |
url | URL | / や . が出やすい |
search | 検索 | 標準 + クリアボタン |
password | パスワード(マスク) | 標準 |
date / time / datetime-local | 日時 | ピッカー |
color | カラー | カラーピッカー |
file | ファイル | ファイル選択 |
range | スライダー | 左右キー |
type="number" の罠
数字なら何でも number にしがちだが、電話番号や郵便番号は tel。
number はスクロール/上下キーで値が変わるので、桁数固定の入力には向かない。
ID 系のコードも text + inputmode="numeric" + pattern が安全。
autocomplete 属性
ブラウザの自動入力やパスワードマネージャ、認証関連の機能はこれを見る。常に書く。
<input type="text" name="name" autocomplete="name" />
<input type="email" name="email" autocomplete="email" />
<input type="tel" name="phone" autocomplete="tel" />
<input type="text" name="zip" autocomplete="postal-code" />
<input type="password" name="pw" autocomplete="current-password" />
<input type="password" name="newpw" autocomplete="new-password" />
<input type="text" name="otp" autocomplete="one-time-code" />
登録フォームの「名前」と「住所」と「電話番号」を1回入力すれば、次回から全部自動で埋まる。 身体的に入力が大変なユーザーには非常に大きな違い。
必須項目の表現
視覚的な印(*)と機械可読な属性の両方を付ける。
<label for="email">
メールアドレス <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />
requiredはネイティブの検証 + スクリーンリーダで「必須」と読み上げ。aria-required="true"は古いSRのフォールバック。最近はrequiredだけで足りる。- 「*」記号は
aria-hiddenで読み上げ重複を防ぐ。 - フォーム冒頭に「* は必須項目」と書いておくのが親切。
エラーメッセージ
エラーは「どのフィールドの」「何が問題か」「どう直せばいいか」を伝える必要がある。
パターン: フィールド近くに表示する
<label for="email">メールアドレス</label>
<input
id="email"
type="email"
required
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" class="error">
正しい形式で入力してください(例: user@example.com)
</p>
aria-invalid="true"でエラー状態を伝える。aria-describedbyでエラーメッセージの id を参照 → SR が「メールアドレス、不正、正しい形式で入力してください」と読む。- エラーが解決したら
aria-invalid="false"に戻し、メッセージは消す。
パターン: フォーム送信時のエラー要約
送信ボタンを押した直後に、エラー一覧をフォーム冒頭に表示してフォーカスを移すと スクリーンリーダ利用者に親切。
<div role="alert" tabindex="-1" id="form-errors">
<h2>3 件のエラーがあります</h2>
<ul>
<li><a href="#email">メールアドレスを入力してください</a></li>
<li><a href="#age">年齢は数字で入力してください</a></li>
</ul>
</div>
<script>
document.getElementById("form-errors").focus()
</script>
パスワード入力
- 貼り付けを禁止しない(パスワードマネージャを阻害する)。
- 表示切替(目アイコン)を提供する。
- ルールは事前に明示。「8文字以上、英大小と数字を含む」のような条件は入力欄の近くに。
autocomplete="current-password"/autocomplete="new-password"を使い分ける。
<label for="pw">パスワード</label>
<input id="pw" type="password" autocomplete="current-password" />
<button type="button" aria-pressed="false" aria-label="パスワードを表示">
<svg aria-hidden="true">...</svg>
</button>
ボタン
- 送信は
type="submit"。 - キャンセル / 開閉などは
type="button"(必ず明示)。 - ラベルが何をするかを表す(「OK」「クリック」より「保存して閉じる」)。
- アイコンだけのボタンには
aria-label。
カスタムウィジェット(select の代替等)
デザインの都合でネイティブ <select> や <input type="date"> を捨てると、
そこから先は全部自分で a11y を組み立てる必要がある:
- キーボード操作(上下キーで選択、Enter で確定、Esc で閉じる)
- focus 管理(メニューが閉じたらトリガに focus を戻す)
- ARIA(
role="combobox"、aria-expanded、aria-activedescendantなど) - スクリーンリーダで意味が伝わるか
必要なければネイティブを使うのが一番。仕様や見た目の制約でやむを得ない時は ARIA Authoring Practices Guide の実装を参考に。 ライブラリなら react-aria / headless UI / Radix UI の利用が安全。
送信前のバリデーション
- HTML 標準のバリデーション(
required/pattern/min/max/type)はそのまま使う。 - 独自バリデーションは
setCustomValidity()で UA 標準に乗せると、エラーメッセージのアクセス手段がそろう。 - ライブラリ(react-hook-form 等)を使うなら、
aria-invalidとaria-describedbyをきちんと当てる。
具体例: ログインフォーム
<form>
<h1>ログイン</h1>
<div role="alert" id="form-error" hidden></div>
<label for="email">メールアドレス</label>
<input
id="email" type="email" name="email"
autocomplete="email" required
aria-describedby="email-hint"
/>
<p id="email-hint" class="hint">登録時のメールを入力</p>
<label for="pw">パスワード</label>
<input
id="pw" type="password" name="password"
autocomplete="current-password" required
/>
<label>
<input type="checkbox" name="remember" />
ログイン状態を保持
</label>
<button type="submit">ログイン</button>
</form>
✓ 全 input に label が紐づいている
✓ placeholder はラベルの代わりになっていない
✓ ラジオ・チェックボックスのグループに fieldset/legend
✓ 適切な type と autocomplete
✓ 必須は required + 視覚的な印
✓ エラー時に aria-invalid + aria-describedby
✓ 送信ボタンに動詞が入った具体ラベル