フォームのバリデーション表示は、:invalidではなく:user-invalidを使うだけで体験が大きく変わります。入力する前から赤枠になる、あの不快な挙動が消えます。2023年10月に Baseline 入りし、現在は全モダンブラウザで使えます。
ひとつ前置きすると、type="email" や required は2023年の新機能ではありません。HTML5(2011年前後)から存在するものです。この記事では、その「もう当たり前の部分」を手早く整理したうえで、2023年以降に実際に使えるようになった機能(:user-invalid、showPicker()、requestSubmit()、field-sizing、accent-color)に紙面を割きます。
スポンサーリンク
フォームで今も土台になる機能(HTML5から)
まず前提の確認です。以下は全ブラウザで長年使えており、JavaScriptを書く前にこれで足りないかを先に考えるべきものです。
| 機能 | 役割 | 登場 |
|---|---|---|
type="email" / url / tel |
形式チェックとモバイルキーボードの切替 | HTML5 |
required |
必須入力 | HTML5 |
pattern |
正規表現による形式指定 | HTML5 |
<datalist> |
入力候補の提示 | HTML5 |
<output> |
計算結果の表示 | HTML5 |
novalidate |
ブラウザ標準の検証を止める | HTML5 |
基本形はこうなります。
<form>
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<label for="zip">郵便番号</label>
<input type="text" id="zip" name="zip"
inputmode="numeric" autocomplete="postal-code"
pattern="\d{3}-?\d{4}"
title="123-4567 または 1234567 の形式で入力してください">
<button type="submit">送信</button>
</form>
ここで押さえておきたい注意点が3つあります。
patternは前後が暗黙にアンカーされます:^と$を書かなくても全体一致です。逆に部分一致にはできませんpatternには必ずtitleを添える:エラーメッセージにこの文言が使われます。無いと「形式が違います」としか出ませんplaceholderをlabelの代わりにしない:入力を始めた瞬間に消えるので、何を入れる欄だったか分からなくなります
そして最も重要な点として、ブラウザ側のバリデーションはセキュリティ対策ではありません。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 |
プレースホルダーが見えているとき | フローティングラベル |
スポンサーリンク
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') {
// 下書き保存の処理
}
});
スポンサーリンク
入力欄を中身の量に合わせて伸縮させる(field-sizing)
テキストエリアを入力量に応じて自動で広げる処理は、これまでJavaScriptでscrollHeightを測って書いていました。CSSのfield-sizing: contentで1行になります。 2026年6月に Baseline 入りしました。
textarea {
field-sizing: content;
min-height: 4lh; /* 最低4行分は確保する */
max-height: 20lh; /* 伸びすぎを防ぐ */
width: 100%;
}
input[type="text"] {
field-sizing: content;
min-width: 8ch;
}
min-heightとmax-heightをセットで指定するのがコツです。指定しないと、空のときに1行分まで縮み、長文で画面いっぱいまで伸びます。lh単位(1行の高さ)を使うと行数で指定できて分かりやすいです。
チェックボックスやスライダーの色を1行で変える
チェックボックス・ラジオ・range・progressの色は、長らくappearance: noneで作り直すしかありませんでした。accent-colorを使えば1行です。
:root {
accent-color: #2a7fff;
}
ブラウザ標準の見た目・キーボード操作・アクセシビリティをそのまま保ったまま、ブランドカラーだけ差し替えられます。見た目のためだけにappearance: noneで自作するのは、フォーカスリングやスクリーンリーダー対応まで自分で再実装することになるので、まずこちらを試してください。
実装時のチェックリスト
フォームを作るときに、私が毎回確認している項目です。
labelとidを必ず結ぶ:placeholderやaria-labelで代用しない(aria-labelは視覚的なラベルが本当に置けない場合の最後の手段)autocompleteを正しく指定する:name/email/tel/postal-code/street-address/one-time-codeなど。入力の手間が目に見えて減りますinputmodeでモバイルキーボードを合わせる:numeric(数字のみ)とdecimal(小数点あり)を使い分ける- エラー表示は
:user-invalidで - 自動送信は
requestSubmit() - サーバー側の検証を必ず実装する
入力欄そのものの属性についてはHTML input要素の新機能と活用法に、確認ダイアログの実装はdialog要素の使い方にまとめています。入力状態に応じたレイアウト制御はCSS :has()セレクターが便利です。
まとめ
type="email"やrequiredはHTML5からの機能で、2023年の新機能ではない- エラー表示は
:user-invalid/:user-validを使う(2023年10月 Baseline) showPicker()で日付・色・候補リストを任意のタイミングで開ける。tryで囲む- 自動送信は
this.submit()ではなくrequestSubmit()。検証もsubmitイベントも通る - テキストエリアの自動リサイズは
field-sizing: content(2026年6月 Baseline) - フォーム部品の色は
accent-color。appearance: noneで自作しない - ブラウザ側の検証はセキュリティ対策ではない
フォームは「JavaScriptを書かずに済ませられる範囲」がこの数年でかなり広がりました。自作の検証ライブラリを入れる前に、標準機能で足りないかを一度確認してみてください。