これまで、CSS セレクタは下方向にしかトラバースできませんでした。つまり、親に基づいて子にスタイルを設定することはできましたが、子に基づいて親にスタイルを設定することはできませんでした。:has() 疑似クラスはこれを変更し、子孫の存在や状態に応じてコンテナ要素のスタイルを条件付きで設定できるようにします。
:has() と状態疑似クラス(:checked、:focus、:valid、:invalid、:not() など)を組み合わせることで、JavaScript で親要素の「修飾子」クラス(.is-active や .has-error など)を切り替えることなく、複雑でインタラクティブな UI コンポーネントをすべて CSS で構築できます。
これは、内部インタラクションに応答する必要があるコンポーネントに特に役立ちます。たとえば、チェックボックス(:checked)に反応するローカライズされたテーマ切り替え、エラー(:invalid)をハイライト表示するフォーム グループ、子リンクがフォーカスされたときにカードを上に移動する(:focus-within)などです。
状態ベースのコンテナ スタイルの実装
必須: コンテナ要素で :has() セレクタを使用して、インタラクティブな子要素の特定の状態(:checked、:focus、:invalid など)を検出する必要があります。
子の状態に基づいてスタイルを変更するコンポーネントを構築するには:
- デフォルトのスタイルを定義する: コンテナに CSS 変数を設定して、基本状態を定義します。
- 状態ベースのオーバーライドを適用する:
:has([child-selector]:[state])を使用してコンテナをターゲットにし、アクティブな状態または代替状態の CSS 変数を再定義します。
例: 子トグルに基づいてテーマを変更するコンポーネント。
/* 1. Define the default state on the component container */
.theme-card {
/* Using custom properties makes state-switching cleaner */
--card-bg: #ffffff;
--card-text: #333333;
--card-border: #cccccc;
background-color: var(--card-bg);
color: var(--card-text);
border: 1px solid var(--card-border);
/* Use a transition for smooth state changes */
transition: background-color 0.3s, color 0.3s;
}
/* 2. Apply styles when the child enters the specific state */
/* MANDATORY: Target the container and use :has() to check the descendant's state */
.theme-card:has(.theme-toggle:checked) {
/* Override the properties for the active or alternate state */
--card-bg: #222222;
--card-text: #f0f0f0;
--card-border: #555555;
}
/* You can also combine :has() and :not() to target specific negative states */
/* This selects a card that DOES NOT have a toggle in the checked state */
.theme-card:not(:has(.theme-toggle:checked)) {
/* Optional: Explicit styles for the unchecked state if needed */
}
<!-- The container element that receives the styling -->
<div class="theme-card">
<!-- The child element whose state controls the parent -->
<label>
<input type="checkbox" class="theme-toggle">
Enable Dark Mode
</label>
<h2>Card Title</h2>
<p>The style of this entire card is controlled by the checkbox above.</p>
</div>
パフォーマンスのヒント: :has() を使用する場合は、セレクタのスコープを可能な限り具体的なコンテナ(.theme-card など)に設定します。スタイルの変更がローカライズされている場合は、body:has(...) などの非常に高レベルの要素にアンカーを設定しないでください。:has() クエリが広範囲に及ぶと、レイアウトの再計算が頻繁に発生する可能性があります。
代替戦略
状態ベースのスタイル設定がユーザー エクスペリエンスやページ レイアウトにとって重要な場合は、:has() セレクタをサポートしていないブラウザ用の代替手段を用意する必要があります。純粋に装飾的な効果を得るには、フォールバックなしのプログレッシブ エンハンスメントとして :has() を使用できます。
必須: 重要な機能の代替を実装する場合は、CSS で @supports not selector(:has(*)) を使用して、従来のクラスベースの代替を定義する必要があります。重要な状態変化がユーザー操作に依存している場合は、CSS.supports() を含む小さなインライン スクリプトを使用して、状態変化を表す同等の JavaScript イベント(change、focus、blur など)に基づいてそのクラスを切り替える必要があります。
/* Fallback CSS for older browsers */
/* We check if the browser DOES NOT support the :has() selector */
@supports not selector(:has(*)) {
/* Define a traditional modifier class that applies the exact same overrides */
.theme-card.is-active {
--card-bg: #222222;
--card-text: #f0f0f0;
--card-border: #555555;
}
}
/* Fallback JavaScript for older browsers */
/* Check for support before running the script to avoid unnecessary work in modern browsers */
if (!CSS.supports('selector(:has(*))')) {
const toggle = document.querySelector('.theme-toggle');
const card = document.querySelector('.theme-card');
if (toggle && card) {
// Manually toggle the fallback class when the input state changes
toggle.addEventListener('change', (e) => {
card.classList.toggle('is-active', e.target.checked);
});
}
}