フォームの検証
誤りは、うまく伝えるより、起こさせないほうが先です。それでも起きたら、何を、なぜ、どう直すかの3つを伝えます。
図の中身を文字で読む
- いつ判定するか
- 入力中 フォーカスが外れた 2回目以降の入力中 送信時
- ✕ 判定しない ○ ここで初回判定 ○ 修正を即座に反映 一覧で示し、各項目へリンク
- 1文字目から判定すると、メールアドレスを書き始めた瞬間に誤りと言われる。
- ✕ 直せないエラー ○ 直せるエラー
- kohara.example.com kohara.example.com
- 入力が正しくありません メールアドレスの形式をご確認ください
- 何が正しくないのか、どう直せばいいのか分からない。 色だけで示すと、色覚特性のある利用者には気づけない。 例: name@example.com
- 何が・なぜ・どう直すかの3点。アイコンと文言を併用する。
ひと目で#
エラーは、伝えるより起こさないほうがよい。
- 制約
- そもそも誤った入力ができない構造にする。事前
- フォーム検証
- 入力された値を判定する。事後
- インライン検証
- 送信を待たず、項目ごとにその場で判定する
- エラーメッセージ
- 何が、なぜ、どう直すかの3点を伝える
判定のタイミング#
| 段階 | やること |
|---|---|
| 入力中(初回) | 判定しない。書き終える前に誤りだと言われることになる |
| フォーカスが外れたとき | ここで初めて判定する |
| 2回目以降の入力中 | 判定する。直ったことをその場で伝える |
| 送信時 | 誤りを先頭に一覧で示し、各項目へリンクを張る |
使い分け#
| こうしたい | やること |
|---|---|
| 過去の日付を選ばせない | 制約(min 属性) |
| 形式を先に伝えたい | ヘルプテキスト。入力の前に見せる |
| 元に戻せる操作の確認 | 確認をやめ、実行したあとに「取り消す」を出す |
| 元に戻せない操作 | 事前の確認ダイアログ |
| 誤りを伝えたい | エラーメッセージに、アイコンと role="alert" |
間違えやすいところ#
図の中身を文字で読む
- ✕ エラーでヘルプを置き換える ○ ヘルプの下に足す
- パスワード パスワード
- ・・・・・ ・・・・・
- 入力が正しくありません 8文字以上、数字を1つ以上
- 数字が入っていません。1つ以上足してください
- 条件を書いた1行が消えている。
- 何文字要るのか確かめる手段が、画面から無くなった。
- エラーはヘルプの代わりではない。
- ヘルプは制約の説明、エラーはいまの入力への指摘で、役割が違う。
- 制約は残したまま、指摘だけを増やす。
- aria-describedby に両方の id を並べて書けば、読み上げも制約と指摘の順に読む。
- 「入力が正しくありません」と書く何が正しくないのか分からない
- 色だけでエラーを示す色覚特性のある利用者に伝わらない
- エラー時に入力内容を消す打ち直しになる
- エラー時にヘルプテキストも消す制約の説明はいつでも要る。並べて残す
- 読み上げに伝えないエラーが起きたことに気づけない
- クライアント側の検証だけで済ませる迂回できる。安全は守られない
- 確認ダイアログを多用する形だけになり、読まれなくなる
直す例#
架空の案件を1つ例にとります。どこを見て、何をどう直すかを書きます。
問い合わせフォームで、送信するとページ先頭に「入力が正しくありません」とだけ出ます。スマホでは該当の欄が画面の外にあり、スクロールして赤い枠を探すしかありません。その赤い枠だけが頼りなので、色の区別がつきにくい利用者には何も起きていないように見えます。
判定を欄ごとに移します。欄を離れたときに初回判定し、誤りは欄の直下に「何が・どう直すか」まで書きます。送信時は先頭に誤りの一覧を出し、各行から該当の欄へリンクを張ります。

文言に迷ったらエラーメッセージの文例へ。よく起きる5つの場面の、悪い文と直した文の対を置きました。
確認する#
- 1文字目を打った瞬間にエラーが出ない。初回の判定は欄を離れたとき
- エラー表示中もヘルプテキストが残っている。エラーは置き換えではなく並べて足されている
- グレースケールで見てもエラーの欄が見つかり、文言だけで直し方まで分かる
使えるパターン#
この話が要る場面#
このページが答えになる、現場の困り事です。困り事の側から探すと、同じ場所に着きます。
何が悪いかと、どう直すかを両方書く。入力欄のそばに置き、色だけに頼らない。
このコードで動いている見本です
入力中は出さない。欄から離れたときか、送信を押したときに出す。
このコードで動いている見本です
この場面のほかの困り事と、そのまま貼れる部品はフォームを作るにまとめてある。逆引きには全188件。
検証を通す#
実際に検証が走るフォームです。空のまま送信するとエラーの一覧と各項目の文言が出て、直せばその場で消え、通れば成功の表示まで進みます。タイミングを切り替えると、同じ誤りでも体感が変わります。
<form id="signup" novalidate>
<div class="form-summary" id="summary" role="alert" tabindex="-1" hidden></div>
<div class="field">
<label for="name">お名前<span>(必須)</span></label>
<input type="text" id="name" name="name" required autocomplete="name"
aria-describedby="name-error">
<p id="name-error" class="error" role="alert"></p>
</div>
<div class="field">
<label for="email">メールアドレス<span>(必須)</span></label>
<input type="email" id="email" name="email" required autocomplete="email"
aria-describedby="email-help email-error">
<p id="email-help" class="help">例: name@example.com</p>
<p id="email-error" class="error" role="alert"></p>
</div>
<p><button type="submit" class="button">送信する</button></p>
</form>
<style>
#signup { display: grid; gap: var(--space-5); }
.field { display: grid; gap: var(--space-2); justify-items: start; }
.field input { font: inherit; padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border); inline-size: min(100%, 22rem); }
.field .help { margin: 0; font-size: var(--text-sm); color: var(--color-text-muted); }
.field .error { margin: 0; font-size: var(--text-sm); color: var(--color-danger); }
.field input[aria-invalid="true"] { border-color: var(--color-danger); }
.form-summary { padding: var(--space-4) var(--space-5);
border: 1px solid var(--color-danger); color: var(--color-danger); }
.form-summary p { margin: 0; font-weight: 700; }
.form-summary ul { margin: var(--space-2) 0 0; padding-inline-start: 1.2em; }
</style>
<script>
var form = document.getElementById("signup");
var summary = document.getElementById("summary");
var fields = [
{ input: document.getElementById("name"),
error: document.getElementById("name-error"),
check: function (v) { return v ? "" : "お名前が入力されていません。"; } },
{ input: document.getElementById("email"),
error: document.getElementById("email-error"),
check: function (v) {
if (!v) return "メールアドレスが入力されていません。";
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
? "" : "メールアドレスの形式をご確認ください。例: name@example.com";
} }
];
function validate(f) {
var message = f.check(f.input.value.trim());
f.error.textContent = message; // 枠は先に置いてあり、中身だけ差し替える
if (message) f.input.setAttribute("aria-invalid", "true");
else f.input.removeAttribute("aria-invalid");
return message;
}
fields.forEach(function (f) {
f.input.addEventListener("blur", function () { // 初回は、書き終えてから判定する
validate(f);
f.input.dataset.touched = "1";
});
f.input.addEventListener("input", function () { // 2回目以降は、直った瞬間に消す
if (f.input.dataset.touched) validate(f);
});
});
form.addEventListener("submit", function (event) {
var bad = fields.filter(function (f) { return validate(f) !== ""; });
if (bad.length === 0) { summary.hidden = true; return; } // 通れば送信される
event.preventDefault();
while (summary.firstChild) summary.removeChild(summary.firstChild);
var head = document.createElement("p");
head.textContent = "入力に " + bad.length + " 件の誤りがあります";
summary.appendChild(head);
var list = document.createElement("ul");
bad.forEach(function (f) {
var item = document.createElement("li");
var link = document.createElement("a");
link.href = "#" + f.input.id;
link.textContent = f.error.textContent;
link.addEventListener("click", function (e) { e.preventDefault(); f.input.focus(); });
item.appendChild(link);
list.appendChild(item);
});
summary.appendChild(list);
summary.hidden = false;
summary.focus(); // 誤りの一覧へ焦点を移す
});
</script>
この道具は JavaScript で動きます。無効の場合は、上の判定のタイミングの表に沿って実装してください。