【実務・中級編】 :invalid 疑似クラス – CSS実践ガイド

おい、調子はどうだ?
今日も元気にフォームのバリデーション実装で頭を悩ませているところか。いや、分かるぞ。バックエンドから返ってきたエラーメッセージの出し分け、JavaScriptでのイベントリスナーの嵐、`touched` や `dirty` といった状態管理のフラグ……。気づけばフロントエンドのコードが、ただの入力チェックのボイラープレートで埋め尽くされている。

「もっとスマートに書けないのかよ」

そんな君の心の声を、俺はエンジニア席の後ろからずっと聞いていた。
結論から言おう。君が夜な夜な書いているそのJSのバリデーションロジック、実はCSSのネイティブ機能だけで、半分以上エレガントに置き換えられるとしたらどうする?

今回は、HTML5のネイティブバリデーションと強力に連携する、`:invalid` 疑似クラスについて徹底的に掘り下げていく。ブラウザの裏側の挙動から、実務で絶対にハマる「あの罠」の回避策まで、シニアの俺がすべて叩き込んでやるから、コーヒーでも飲みながらじっくり読んでくれ。

—

1. `:invalid` 疑似クラスの正体と、ブラウザの裏側の話

まず、`:invalid` とは何か。公式ドキュメント的な説明をサラッとしておくと、「HTMLのバリデーション属性(`required`, `type=”email”`, `pattern` など)のルールを満たしていないフォーム要素」にマッチする疑似クラスだ。

だが、ここで中級のエンジニアたちが最初に引っかかる罠がある。

「おい、ページを開いた瞬間、まだ何も入力していない空の必須フィールド(`required`)にいきなり `:invalid` が適用されて、画面中が真っ赤になったぞ!これじゃ使えねぇよ!」

そう、鋭いな。それこそが、ブラウザが裏側でどうDOMを評価しているかを知るべきポイントだ。

ブラウザはどうやって状態を判定しているのか?

ブラウザのエンジン(BlinkやGeckoなど)は、フォーム要素の「妥当性(Validity State)」を常時トラッキングしている。HTML5の制約検証API(Constraint Validation API)の内部ステータスだ。
要素が生成された瞬間、`required` がついているのに値が空であれば、ブラウザは心の中でこう叫ぶ。
「おい、ここ必須だぞ。空だからルール違反(`ValidityState.valueMissing = true`)だな!」
結果として、要素は `:invalid` の烙印を押される。

ここにスタイリングを直結させると、ユーザーがまだ入力すら始めていないロード直後から、エラーの赤い枠線で警告されるという最悪のUXが完成するわけだ。……おい、笑い事じゃないぞ。実務の現場でも、この罠に気づかずに実装してQAチームに差し戻されているやつを何人も見てきた。

じゃあ、どうするか?
ここで登場するのが、CSSとHTMLの「状態」を組み合わせるテクニックだ。

—

2. 現場で使える! `:invalid` のスマートな飼い慣らし方

実務で `:invalid` を使うときの鉄則は一つ。
「ユーザーが操作したあと(あるいは、送信を試みたあと)、かつ入力中のもの」に対してのみスタイルを適用することだ。

無闇矢鱈に `:invalid` 単体で書くのは素人のやること。プロは、疑似クラスのコンボ技でブラウザの機嫌を取る。

実務でそのまま使える!モダンなフォームスタイリング

以下のコードを見てほしい。コピペして手元の環境で動かしてみれば、その洗練された挙動に感動するはずだ。



有効なメールアドレスを入力してください。

/ CSS /

スコープを明確にするためにベースのスタイルを定義 /
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 400px;
font-family: sans-serif;
}

.form-group input {
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}

/ 1. 通常時のフォーカス状態 /
.form-group input:focus {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15);
}

/
2. 【重要】ユーザーが実際に「入力中(user-invalid)」であり、
かつフォーカスが外れたタイミング(あるいは送信試行後)にエラー表示をする。
ここで :user-invalid を使うのが令和のフロントエンドの最適解だ。
/
.form-group input:user-invalid {
border-color: #d32f2f;
background-color: #fdf2f2;
}

/ 3. エラーメッセージはデフォルトで隠しておく /
.form-group .error-message {
display: none;
font-size: 0.875rem;
color: #d32f2f;
}

/ 4. ユーザーが不正な入力をしている最中だけ、メッセージをスッと出現させる /
.form-group input:user-invalid ~ .error-message {
display: block;
}

—

3. 次世代の切り札:`:user-invalid` との使い分け

お気づきだろうか。先ほどのコードで、俺はあえて `:invalid` ではなく `:user-invalid` という、一見すると見慣れない疑似クラスを使った。

ここが今回の記事で一番伝えたい、チーフアーキテクトからの最大のギフトだ。

`:invalid` と `:user-invalid` の決定的な違い

  • `:invalid`:

要素のバリデーションルールが満たされていない「客観的な事実」だけでマッチする。ユーザーがその入力欄に触れていようがまい関係ない。

  • `:user-invalid`:

`:invalid` の条件に加え、「ユーザーがその要素を操作した(インタラクションがあった)」という文脈を含めてマッチする。具体的には、一度フォーカスして値を変更し、そのバリデーションが破綻した時だ。

この `:user-invalid`(およびその対である `:user-valid`)は、Selectors Level 4で策定されたモダンな疑似クラスで、主要ブラウザでも完全に対応が進んでいる。

これによって何が嬉しいか?
「最初から赤枠が出ているストレスフルなUI」を、CSSの一行の書き換えだけで完璧に防げるようになる。 JavaScriptで「`is-touched` クラスをstate管理して……」なんて泥臭いコードを書く必要は、もう極めて少なくなったんだ。

—

4. チーフアーキテクトが教える、実務での注意点と落とし穴

さて、ここまで聞くと「じゃあ全部CSSに任せてJSのバリデーションは捨てようぜ!」と思うかもしれないが、焦るな。現場のシステムはそんなに単純じゃない。

最後に、実務で `:invalid` / `:user-invalid` を扱う上での注意点をいくつか共有しておこう。

1. フォーム全体の送信制御(`form:invalid`)の罠
実は、フォーム要素だけでなく `

` タグ自体にも `:invalid` を付与できる(子要素に無効なフィールドがある場合)。これを利用して「入力に不備がある間は送信ボタンを非活性(disabled)にする」というアプローチがよくあるが、UXの観点からはあまりおすすめしない。 ユーザーが「なぜボタンが押せないのか」理由を理解できないフラストレーションを生むからだ。ボタンは常に活性化しておき、押されたタイミングでブラウザのネイティブ挙動や `:user-invalid` にフォーカスを当てる方が、アクセシビリティの観点からも健全だ。
2. スタイルのリセットとブラウザの差異
ブラウザによっては、`:invalid` な要素に対してデフォルトで独自の赤色のシャドウやアウトラインを付与するものがある(Firefoxなど)。意図しないデザインの崩れを防ぐために、CSSの初期化段階で `box-shadow: none;` や適切な枠線のリセットを忘れないようにしよう。

—

まとめ

CSSの疑似クラスは、単なる「見た目を飾る道具」ではない。
ブラウザが持っているセマンティクスや状態(State)をダイレクトに引き出し、宣言的にUIをコントロールするための強力なロジック層だ。

`:invalid` や `:user-invalid` を使いこなせるようになると、JavaScriptのコードベースから「入力状態の監視」という名の無駄なノイズがスルスルと消えていく。コードが軽くなり、パフォーマンスが上がり、何より保守性が劇的に向上する。

さあ、理屈はここまでだ。
明日から、いや、今書いているその機能から、不要なJSのバリデーションフラグを削ぎ落として、ネイティブのCSSに仕事を奪い返してやれ。

君のコードが、もっと洗練されるのを期待している。

コメント

タイトルとURLをコピーしました