フロントエンドの現場で日々フォームと格闘している君なら、JavaScriptでちまちま正規表現を書いて、`input`イベントのたびにエラー判定して……という泥臭い実装に、一度は嫌気が差したことがあるはずだ。
「これ、本当にJSでやらなきゃいけないんだっけ?」
そう思った君へ朗報だ。モダンブラウザのネイティブ機能であるCSSの `:valid` と `:invalid` 擬似クラスを使いこなせば、HTML5のバリデーション属性(`required`, `pattern`, `minlength` など)と連動して、ブラウザが勝手に状態を判定し、スタイリングまで完結させてくれる。JSのボイラープレートコードをごっそり削ぎ落とせる、まさに現場の秘密兵器だ。
今回は、このネイティブバリデーション連動の極意を、実務で即座に使えるレベルの知見とともに叩き込んでいこう。
—
1. 裏側で何が起きているか? ブラウザのレンダリングメカニズム
まず、ブラウザが裏側でどう動いているかを知る必要がある。なんとなく使っていると「あれ、ページを開いた瞬間にエラーの赤枠が出ちゃったんだけど!」という、デザイナーからの恐怖のフィードバックを受けることになるからだ。
状態の判定アルゴリズム
ブラウザは、フォーム要素(`` や `
- `:valid`: 入力値が指定されたバリデーションルール(`required`, `type=”email”`, `pattern=”…”` など)を完全に満たしている状態。
- `:invalid`: ルールを満たしていない状態。
ここで重要なのが、「ユーザーがまだ何も入力していない初期状態(空っぽ)」の扱いだ。
`required` がついたインプットは、当然ながら最初は空。つまり、ブラウザの判定としては「ルールを満たしていない(未入力)」ため、初期状態から容赦なく `:invalid` が発火する。
実務でこれをそのままCSSに反映させると、ページを開いた瞬間に全ての必須項目が赤く染まるという、ユーザービリティの暴力が完成してしまう。これをどういなすかが、プロとアマの分かれ道だ。
—
2. 現場で使える!実践的コードサンプル
百聞は一見にしかず。まずは、余計なJSを一切使わずに、美しくインタラクティブにバリデーションを表現するCSSとHTMLのコードを見てほしい。
HTML
※ `novalidate` はブラウザデフォルトのポップアップツールチップを抑制するためにつけている。デザインされたUIでエラー表現を統一したい場合は必須のテクニックだ。
CSS
/ — ベースのスタイル — /
.p-form {
max-width: 400px;
margin: 40px auto;
font-family: sans-serif;
}
.p-form__field {
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
.p-form__label {
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.p-form__input {
padding: 12px;
font-size: 16px;
border: 2px solid #ccc;
border-radius: 6px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/ — エラーメッセージの初期状態(隠しておく) — /
.p-form__error-message {
display: none;
font-size: 12px;
color: #e53e3e;
margin-top: 6px;
}
/
==========================================================================
ここからが本題::user-invalid との組み合わせの妙
==========================================================================
/
/
【アンチパターン】
input:invalid とだけ書くと、ページ読み込み直後から赤枠になるので絶対に避けること!
/
/
【ベストプラクティス】
:user-invalid (または :placeholder-shown との組み合わせ) を使う。
今回は広く普及している `:placeholder-shown` の否定形を用いた実践テクニックを使う。
ユーザーが「一度でもフォーカスして、値を入力し始めた(=プレースホルダーが消えた)」かつ「不正な値」の時だけ発火させる。
/
.p-form__input:not(:placeholder-shown):invalid {
border-color: #e53e3e;
background-color: #fff5f5;
}
/ 正常に入力された瞬間 /
.p-form__input:not(:placeholder-shown):valid {
border-color: #38a169;
background-color: #f0fff4;
}
/ 不正な値の時だけエラーメッセージをスライドイン的に表示 /
.p-form__input:not(:placeholder-shown):invalid ~ .p-form__error-message {
display: block;
}
—
3. シニアが教える、現場のハマり所とハック
上記のコードで使った `:not(:placeholder-shown)` というセレクタのコンビネーション、これが実務では非常に強力なハックになる。
ハックの解説:なぜ `:not(:placeholder-shown)` なのか?
先ほど「ページを開いた瞬間から `:invalid` が発火する」という問題に触れた。
これを回避するモダンな標準仕様として `:user-invalid` という擬似クラスも策定されているが、ブラウザのサポート状況や挙動の微調整(ユーザーがフォーカスを外した瞬間か、入力中かなど)において、現場ではまだコントロールしきれない部分がある。
そこで、「プレースホルダーが表示されている=まだユーザーが入力していない」という事実を利用する。
- 初期状態:プレースホルダーが表示されている → `:not(:placeholder-shown)` は偽。したがって `:invalid` のスタイルは適用されない。
- 入力開始:文字を打つとプレースホルダーが消える → `:not(:placeholder-shown)` が真になり、かつ条件を満たしていなければ `:invalid` が適用される!
この一手間を加えるだけで、「最初から赤らんだ不親切なフォーム」を完璧に防ぐことができる。
—
4. まとめ:JSによるDOM操作の呪縛から解放されよう
フォームのバリデーション制御をすべてJSに頼り切っていた時代はもう終わった。
ブラウザのネイティブな仕組みである `:valid` と `:invalid` をCSSのセレクタで適切に制御できれば、以下のメリットが手に入る。
1. パフォーマンスの向上: JSのイベントリスナー(`input`, `blur`など)を毎度仕込む必要がなくなり、メインスレッドが軽くなる。
2. コード量の削減: DOMのクラス付け替え(`element.classList.add(‘is-error’)`など)を記述するボイラープレートから解放される。
3. 宣言的なスタイリング: 「状態」をCSSに任せることで、マークアップとデザインの関心の分離が美しく保たれる。
もちろん、サーバーサイドとの複雑なバリデーション連携や、非同期の重複チェック(「このメールアドレスは既に使われています」など)はJSの領域だ。しかし、「文字数制限」や「必須チェック」「正規表現によるフォーマット確認」といったクライアント側の一次判定は、CSSに任せるのがこれからのモダンなフロントエンドの作法と言える。
さあ、次のスプリントのタスクで、ボッチのJSコードを消し去り、この洗練されたCSSに置き換えてみせよう。君のコードレビューを受ける後輩たちも、きっとそのスマートさに唸るはずだ。

コメント