フォームのバリデーション表示は、:invalidではなく:user-invalidを使うだけで体験が大きく変わります。入力する前から赤枠になる、あの不快な挙動が消えます。2023年10月に Baseline 入りし、現在は全モダンブラウザで使えます。
この記事では、フォームの「検証」と「送信」に話を絞って、標準機能だけでどこまで書けるかを整理します。扱うのは :user-invalid / :user-valid、setCustomValidity()、showPicker()、requestSubmit() です。入力欄そのものの属性や type の選び方は、それぞれ別記事に分けました。
スポンサーリンク
検証の土台はHTML5の機能で足りる
まず前提の確認です。以下は全ブラウザで長年使えており、JavaScriptを書く前にこれで足りないかを先に考えるべきものです。type="email" や required は新しい機能ではなく、HTML5(2011年前後)から存在します。
| 機能 | 役割 | 登場 |
|---|---|---|
type="email" / url / tel |
形式チェックとモバイルキーボードの切替 | HTML5 |
required |
必須入力 | HTML5 |
pattern |
正規表現による形式指定 | HTML5 |
minlength / maxlength |
文字数の下限・上限 | HTML5 |
novalidate |
ブラウザ標準の検証を止める | HTML5 |
基本形はこうなります。
<form>
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<button type="submit">送信</button>
</form>
patternで形式を指定する場合は、次のようにtitleを添えます。
<input type="text" name="zip" pattern="\d{3}-?\d{4}"
title="123-4567 または 1234567 の形式で入力してください">
ここで押さえておきたい注意点が3つあります。
patternは前後が暗黙にアンカーされます:^と$を書かなくても全体一致です。逆に部分一致にはできませんpatternには必ずtitleを添える:エラーメッセージにこの文言が使われます。無いと「形式が違います」としか出ませんplaceholderをlabelの代わりにしない:入力を始めた瞬間に消えるので、何を入れる欄だったか分からなくなります
なお、上の郵便番号の例はpatternの説明に絞った最小形です。実際にはinputmode="numeric"とautocomplete="postal-code"も必要で、そのあたりはinputmode・autocomplete・enterkeyhintでスマホの入力を速くするにまとめています。<datalist>や<select>の使い分け、typeごとの癖はinput要素のtype一覧と使い分けを参照してください。
そして最も重要な点として、ブラウザ側のバリデーションはセキュリティ対策ではありません。DevToolsでnovalidateを足せば誰でも回避できます。サーバー側の検証は必ず別途実装してください。
:user-invalid で「入力前に赤くなる」問題を解決する
:invalidの最大の欠点は、ページを開いた瞬間から未入力の必須項目が全部エラー表示になることです。まだ何もしていないユーザーを叱っている状態になります。
:user-invalidは、ユーザーが実際に操作して離れたあとにしかマッチしません。
/* ❌ 開いた瞬間から赤くなる */
input:invalid {
border-color: #d33;
}
/* ✅ ユーザーが触ったあとだけ赤くなる */
input:user-invalid {
border-color: #d33;
}
input:user-valid {
border-color: #2a7;
}
エラーメッセージの出し分けも、CSSだけで書けます。
<div class="field">
<label for="mail">メールアドレス</label>
<input type="email" id="mail" name="mail" required>
<p class="error">正しいメールアドレスを入力してください</p>
</div>
.error { display: none; color: #d33; font-size: .875rem; }
input:user-invalid ~ .error { display: block; }
| セレクター | マッチする条件 | 使いどころ |
|---|---|---|
:invalid |
値が不正なら常に | 送信ボタンの:has()判定など |
:user-invalid |
ユーザーが操作したあとで不正なとき | 入力欄の見た目 |
:user-valid |
ユーザーが操作したあとで正しいとき | チェックマーク表示 |
:placeholder-shown |
プレースホルダーが見えているとき | フローティングラベル |
スポンサーリンク
エラーメッセージを自分の言葉にする
ブラウザ標準のメッセージ(「このフィールドを入力してください」)は、そのままでは不親切なことがあります。setCustomValidity()で差し替えられます。
const zip = document.getElementById('zip');
zip.addEventListener('input', () => {
// 一度クリアしないと、直しても不正なままになる
zip.setCustomValidity('');
if (zip.validity.valueMissing) {
zip.setCustomValidity('郵便番号を入力してください');
} else if (zip.validity.patternMismatch) {
zip.setCustomValidity('ハイフンありなしどちらでも構いません(例: 123-4567)');
}
});
先頭でsetCustomValidity('')を呼ぶのが必須です。空文字を渡すまでその入力欄は「不正」のままになり、正しく直しても送信できなくなります。これは非常によく踏むバグです。
validityオブジェクトで、不正の理由を細かく判別できます。
| プロパティ | 意味 |
|---|---|
valueMissing |
requiredなのに空 |
typeMismatch |
emailやurlの形式違反 |
patternMismatch |
patternに一致しない |
rangeUnderflow / rangeOverflow |
min / max の範囲外 |
tooShort / tooLong |
minlength / maxlength 違反 |
stepMismatch |
stepの刻みに合わない |
showPicker() で日付・色・候補リストを任意のタイミングで開く
<input type="date">のカレンダーは、既定では小さなアイコンを正確にクリックしないと開きません。showPicker()を使うと、入力欄のどこを押してもピッカーを開けます。2022年9月から全モダンブラウザで使えます。
const dateInput = document.getElementById('date');
dateInput.addEventListener('click', () => {
if (!('showPicker' in HTMLInputElement.prototype)) return;
try {
dateInput.showPicker();
} catch (e) {
// NotAllowedError など。既定の挙動に任せる
}
});
対応するのは date / month / week / time / datetime-local / color / file、そして<datalist>を持つ入力欄です。
制約が3つあるので、必ずtryで囲んでください。
- ユーザー操作が起点でないと
NotAllowedError:setTimeoutから呼ぶことはできません - クロスオリジンのiframe内では
SecurityError(fileとcolorは例外) readonlyやdisabledだとInvalidStateError
スポンサーリンク
自動送信は this.submit() ではなく requestSubmit()
絞り込みセレクトの変更で自動送信する、という実装はよくありますが、this.submit()を使ってはいけません。
<!-- ❌ バリデーションが全て無視される、submitイベントも発火しない -->
<form onchange="this.submit()">
...
</form>
form.submit()はバリデーションを飛ばし、submitイベントも発火させません。そのため、他のスクリプトが仕掛けた送信前処理(二重送信防止、トークン付与など)が全て素通りします。
正しくはrequestSubmit()です。こちらは「送信ボタンを押したのと同じ」扱いになります。
<form id="filter-form" action="/search">
<select name="category">
<option value="all">すべて</option>
<option value="book">本</option>
</select>
<noscript><button type="submit">絞り込む</button></noscript>
</form>
const form = document.getElementById('filter-form');
form.addEventListener('change', () => {
form.requestSubmit(); // 検証もsubmitイベントも通る
});
あわせて、submitイベントではどのボタンで送信されたかをevent.submitterで取れます。「下書き保存」と「公開」を1つのフォームで捌くときに便利です。
form.addEventListener('submit', (e) => {
if (e.submitter?.value === 'draft') {
// 下書き保存の処理
}
});
送信ボタンは<input type="submit">ではなく<button type="submit">で書いてください。ボタン内にアイコンや複数行を入れられます。
検証と送信のチェックリスト
フォームを作るときに、検証と送信まわりで私が毎回確認している項目です。
- エラー表示は
:user-invalidで::invalidだと開いた瞬間に全部赤くなります patternにはtitleを添える:これがエラーメッセージになりますsetCustomValidity()は毎回空文字でクリアしてから設定する- 自動送信は
requestSubmit():submit()は検証もsubmitイベントも飛ばします labelとidを必ず結ぶ:placeholderやaria-labelで代用しない(aria-labelは視覚的なラベルが本当に置けない場合の最後の手段)- サーバー側の検証を必ず実装する:ブラウザ側の検証はセキュリティ対策ではありません
入力の速さに効く属性(inputmode / autocomplete / enterkeyhint)はinputmode・autocomplete・enterkeyhintでスマホの入力を速くする、typeごとの選び方とfield-sizing・accent-colorはinput要素のtype一覧と使い分けにまとめています。確認ダイアログの実装はdialog要素の使い方、入力状態に応じたレイアウト制御はCSS :has()セレクターが便利です。
まとめ
type="email"やrequiredはHTML5からの機能。JavaScriptの前にまずこれで足りるか考える- エラー表示は
:user-invalid/:user-validを使う(2023年10月 Baseline) patternは暗黙に全体一致。titleがエラーメッセージになるsetCustomValidity()は毎回空文字でクリアしてから設定するshowPicker()で日付・色・候補リストを任意のタイミングで開ける。tryで囲む- 自動送信は
this.submit()ではなくrequestSubmit()。検証もsubmitイベントも通る - どのボタンで送信されたかは
event.submitterで判別できる - ブラウザ側の検証はセキュリティ対策ではない
フォームは「JavaScriptを書かずに済ませられる範囲」がこの数年でかなり広がりました。自作の検証ライブラリを入れる前に、標準機能で足りないかを一度確認してみてください。