【実務・中級編】 has擬似クラスの実務デザインパターン – CSS実践ガイド

おい、調子はどうだ?
今日も元気に `!important` で汚されたレガシーコードと格闘してないか?

なあ、フロントエンドの現場にいる中級者のお前らなら、一度はこう思ったことがあるはずだ。
「おい、子要素の状態を親に伝えてスタイルを変えたいだけなのに、なんでわざわざJavaScriptを回さなきゃいけないんだよ!」ってな。

かつてのCSSは「親から子へ」一方通行の冷酷な世界だった。親の背中を見て子は育つが、親は子どもの顔色を一切窺わない。そんな時代だったんだ。
だが、もうそんな無駄なJavaScriptのハックや、クラス名の付け替え地獄に怯える必要はない。救世主はすでに標準化され、すべてのモダンブラウザでバリバリ動く。そう、`:has()` 擬似クラス、通称「親セレクタ」の登場だ。

今日は、この `:has()` を使って、現場のコードを劇的に美しく、かつマッスルにするための実践的なデザインパターンをいくつか授けよう。気合入れてついてこいよ。

—

1. なぜ今 `:has()` なのか? ブラウザの裏側の話

まず、こいつの凄みを語る前に、ブラウザが裏側でどう動いているかを知っておく必要がある。中級者なら、ここを理解しておかないと「魔法の杖」として振り回してパフォーマンスを落とすハメになるからな。

CSSセレクタの基本は、実は「右から左(Right-to-Left)」に評価される。
例えば `.card h2` というセレクタがあったら、ブラウザはまずDOMツリーの中からすべての `h2` を探し出し、「おや、お前たちの親に `.card` はいるかい?」と上に向かって(ツリーを逆算して)確認していく。

`:has()` が登場したことで、この評価はさらに強力になった。
`:has()` は、指定された条件に一致する子孫要素(または後続要素)が存在するかどうかを判定し、その親要素(または先行要素)にスタイルを適用する。

「それって重いんじゃないの?」と心配するお前、鋭いな。
確かに、DOMの深くまでゴリゴリ再帰的に走査するような複雑すぎるセレクタを書けば、レンダリングのパフォーマンスに影響を与える可能性はある。だが、実務で使う一般的なコンポーネント単位のスコープであれば、ブラウザ側の最適化(BlinkやGeckoのエンジンレベルでのキャッシュ機構)のおかげで、JavaScriptでゴリゴリDOMを監視する(Intersection ObserverやMutationObserverを回す)よりも、圧倒的に高速で省メモリだ。

JavaScriptのイベントリスナーを削ぎ落とし、スタイリングの関心を完全にCSSの世界に閉じ込める。これこそが、現代のフロントエンドアーキテクチャのあるべき姿なのだ。

—

2. 実務で即採用できる `:has()` デザインパターン

前置きはこれくらいにして、現場で明日から使える具体的なコードを見せていこう。どれも俺たちが毎日のように頭を悩ませてきた実用的なケースばかりだ。

パターンA:フォームのバリデーション状態に応じたラベル&枠線の動的スタイリング

フォームの実装で、入力エラーが起きたときに親の wrapper ごと赤枠にしたり、エラーメッセージを出したりする処理。昔はJSで `.is-error` みたいなクラスをバシバシ付け替えてただろ?
`:has()` を使えば、HTMLの構造そのものはクリーンなまま、CSSだけで完結する。



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

/ フォームグループの基本スタイル /
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
transition: border-color 0.2s ease;
}

/ 通常のインプット枠 /
.form-group input {
padding: 0.75rem;
border: 1px solid #cbd5e1;
border-radius: 4px;
}

/ ==========================================
ここが肝:入力値が不正かつバリデーションが発火した状態
================————————– /
/ :invalid かつ :placeholder-shown が「偽」のとき(=ユーザーが何か入力した、またはフォーマットエラー) /
.form-group:has(input:focus:invalid) {
–accent-color: #ef4444; / エラーカラー /
}

/ 入力欄が不正な状態のとき、親の枠線とラベルの色を変える /
.form-group:has(input:invalid:not(:placeholder-shown)) {
–accent-color: #ef4444;
}

.form-group:has(input:invalid:not(:placeholder-shown)) input {
border-color: #ef4444;
background-color: #fef2f2;
}

.form-group:has(input:invalid:not(:placeholder-shown)) .error-message {
display: block; / エラーメッセージを表示 /
color: #ef4444;
font-size: 0.875rem;
}

/ デフォルトではエラーメッセージは隠しておく /
.error-message {
display: none;
}

どうだ? JavaScriptを1行も書かずに、ユーザーの入力状態に応じて親要素のコンテキスト(枠線やエラー文言の表示)をコントロールできている。これがスマートな現代のCSSだ。

—

パターンB:画像(メディア)の有無によるカードレイアウトの自動可変

次は、CMSから出力されるようなカードUIだ。
「アイキャッチ画像がある記事カード」と「テキストだけの記事カード」が混在するリストを想像してくれ。画像があるときは2カラムのレイアウトにし、画像がないときはテキストを全幅(1カラム)に広げたい。
従来ならサーバーサイドやJSで `has-thumbnail` みたいなクラスを付与していたはずだ。

サムネイル

タイトルが入ります

ここに本文の抜粋が少し入ります…

テキストだけのタイトル

アイキャッチ画像がない場合は、このようにレイアウトを柔軟に変えたい。

.card {
display: grid;
grid-template-columns: 1fr; / デフォルトは1カラム(画像なしをベースにする) /
gap: 1rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}

/ ==========================================
:has() を使って、子孫に .card__thumb が「いる」場合のみ
グリッドの定義を書き換える
================————————– /
.card:has(.card__thumb) {
grid-template-columns: 240px 1fr; / 画像エリアとテキストエリアの2カラムに変化 /
}

.card__thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}

.card__body {
padding: 1.5rem;
}

ベースの設計を「画像なし(シンプル)」にしておき、`:has(.card__thumb)` でリッチなレイアウトに拡張する。この「プログレッシブ・エンハンスメント(段階的改良)」の思想こそ、シニアが好む美しいCSSアーキテクチャだ。

—

パターンC:チェックされた子要素を持つコンテナのハイライト(リスト全体の状態管理)

最後は、テーブルやタスクリストの行選択UIだ。
チェックボックスが `checked` になったとき、その親である行(`tr` や `li`)の背景色を変えるだけでなく、行内のテキストを打ち消し線付きにしたり、不透明度を下げたりするパターンを見てみよう。

.task-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #cbd5e1;
border-radius: 6px;
}

.task-item {
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.15s ease;
}

.task-item:last-child {
border-bottom: none;
}

/ ==========================================
子要素の input が checked のとき、親の li 全体をスタイリング
================————————– /
.task-item:has(input[type=”checkbox”]:checked) {
background-color: #f8fafc;
}

/ チェックされたら、中のテキストをグレーアウトして打ち消し線を入れる /
.task-item:has(input[type=”checkbox”]:checked) span {
color: #94a3b8;
text-decoration: line-through;
}

親から子への一方通行だった世界から、`:has()` によって「子は親を支配し、親は子を包み込む」という双方向のコンテキスト共有が可能になった。これはもう、CSSの歴史におけるパラダイムシフトと言っていい。

—

3. 実務で使うときの注意点(シニアからのアドバイス)

さて、ここまでベタ褒めしてきた `:has()` だが、現場で使う上での「お約束(落とし穴)」もいくつかある。最後にこれだけは頭に叩き込んでおいてくれ。

1. 過剰なネストは避けること
`main:has(section:has(article:has(div:has(p))))` みたいなどこぞの魔窟のようなセレクタを書いた瞬間、コードレビューで俺に弾かれると思え。可読性が死ぬし、メンテナンス性も最悪になる。コンポーネント単位のスコープ(親と直近の子の関係性)に留めるのが美学だ。
2. パフォーマンスの過信は禁物
何千、何万という要素を持つ巨大なリストの各行に対して複雑な `:has()` を多用すると、スクロール時などにレイアウトの再計算(リフロー)コストが跳ね上がる。適材適所、必要最低限の範囲で使うこと。
3. ブラウザサポートの確認
Safari、Chrome、Edge、Firefoxともに主要なモダンブラウザでは完全にサポートされている。IE? そんなものは存在しない(もしサポートが必要なら、お前は今すぐタイムマシンに乗るか、レガシー案件から逃げ出すべきだ)。

—

さあ、理屈は分かったな。
明日から「子要素の状態が変わるからJSでクラスを付け替えよう」なんてコードを書いたら、俺が背後からそっとコーヒーを差し入れつつ「それ、`:has()` でいけるよ?」と優しく囁いてやるから覚悟しておけ。

CSSは日々進化している。お前も過去のやり方に固執せず、常に最先端のスマートな設計を取り入れていこうぜ。それじゃ、コーディングに戻るとするか!

コメント

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