ジャンルから探す

Web Design English

HTMLフォームのバリデーションと送信|:user-invalidとrequestSubmit()

フォームのバリデーション表示は、: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を書かずに済ませられる範囲」がこの数年でかなり広がりました。自作の検証ライブラリを入れる前に、標準機能で足りないかを一度確認してみてください。