CSS :has() セレクター実践:親セレクターがコンポーネントスタイルアーキテクチャを変える

前端工程(更新: 2026年6月9日)

:has() セレクター:CSS の「親セレクター」

長らく CSS は親から子へしか選択できませんでした。:has() はこの制限を打破し、子から親への逆方向選択を実現します:

/* 従来:子要素しか選択できない */
.card .title { color: blue; }

/* :has():子要素の状態に基づいて親要素を選択 */
.card:has(.title) { border-color: blue; }

ブラウザ対応

ブラウザ 対応バージョン リリース
Chrome 105+ 2022.08
Safari 15.4+ 2022.03
Firefox 121+ 2023.12
Edge 105+ 2022.08

2026 年時点で、すべての主要ブラウザが :has() に対応しています。


構文とマッチングロジック

基本構文

/* 指定の子孫を含む */
parent:has(descendant) { }

/* 直接の子を含む */
parent:has(> child) { }

/* 兄弟要素が存在 */
element:has(+ sibling) { }
element:has(~ sibling) { }

/* 複数条件の組み合わせ */
element:has(.a, .b) { }    /* OR */
element:has(.a):has(.b) { } /* AND */

:not() との組み合わせ

/* .active 子要素を含まない .nav */
.nav:not(:has(.active)) {
  opacity: 0.5;
}

/* 未チェックの checkbox を含む */
.form:has(input:not(:checked)) {
  border-color: orange;
}

実践例 1:フォームバリデーション状態

従来の JS アプローチ

// 各入力のバリデーション状態をリスニング
input.addEventListener('invalid', () => {
  form.classList.add('has-error');
});
input.addEventListener('valid', () => {
  form.classList.remove('has-error');
});

:has() アプローチ

/* 無効な入力を含むフォーム:全体スタイル変化 */
form:has(:user-invalid) {
  border-color: #ef4444;
}

form:has(:user-invalid) .submit-btn {
  opacity: 0.5;
  pointer-events: none;
}

/* 個別の無効フィールド:コンテナをハイライト */
.field:has(:user-invalid) {
  background: #fef2f2;
}

.field:has(:user-invalid) .error-msg {
  display: block;
}

/* 全フィールド有効:成功メッセージ表示 */
form:has(.field:not(:has(:user-invalid))) .success-msg {
  display: block;
}
<form>
  <div class="field">
    <label>メール</label>
    <input type="email" required />
    <span class="error-msg">有効なメールアドレスを入力してください</span>
  </div>
  <div class="field">
    <label>パスワード</label>
    <input type="password" required minlength="8" />
    <span class="error-msg">パスワードは8文字以上</span>
  </div>
  <button class="submit-btn">送信</button>
  <span class="success-msg">フォームは有効です</span>
</form>

実践例 2:カードレイアウト

画像あり・なしのカード自适应

/* 画像ありカード:水平レイアウト */
.card:has(img) {
  display: flex;
  gap: 1rem;
}

.card:has(img) .card-body {
  flex: 1;
}

/* 画像なしカード:垂直レイアウト */
.card:not(:has(img)) {
  display: block;
  text-align: center;
}

/* 横長画像:比率調整 */
.card:has(img[width="1200"]) {
  flex-direction: column;
}

カードホバー効果

/* カードのどこにホバーしても画像が拡大 */
.card:has(.card-link:hover) img {
  transform: scale(1.05);
}

.card:has(.card-link:hover) .card-title {
  color: var(--primary);
}

実践例 3:ナビハイライト

従来:サーバー側で active クラスを付与

<nav>
  <a href="/" class="active">ホーム</a>
  <a href="/blog">ブログ</a>
</nav>

:has() アプローチ:純 CSS で現在ページを感知

/* aria-current 属性を利用 */
nav:has([aria-current="page"]) {
  border-bottom-color: var(--primary);
}

nav a:has(+ [aria-current="page"]),
nav a[aria-current="page"] {
  color: var(--primary);
  font-weight: 600;
}

/* アクティブ項目がある時のみ戻るボタン表示 */
nav:not(:has([aria-current="page"])) .back-btn {
  display: none;
}

サイドバ折りたたみ感知

/* 展開されたサブメニューがある時:区切り線表示 */
.sidebar:has(.submenu:not([hidden])) {
  border-right: 2px solid var(--border);
}

/* 全サブメニューが折りたたまれている時:コンパクトモード */
.sidebar:not(:has(.submenu:not([hidden]))) {
  width: 64px;
}

実践例 4:ダークモード連動

画像の明るさに応じて自動調整

/* 高輝度画像を含む時:周囲のテキストを暗く */
figure:has(img.light) {
  background: #1a1a1a;
  color: #fff;
}

/* ダークモードで透過ロゴに背景を追加 */
@media (prefers-color-scheme: dark) {
  .container:has(img[alt*="logo"]) {
    background: #fff;
    padding: 8px;
    border-radius: 8px;
  }
}

実践例 5:テーブルとリストのインタラクション

テーブル全選感知

/* 全チェックボックス選択時:ヘッダースタイル変化 */
table:has(tbody input:checked:not(:indeterminate):not(:only-child))
  th {
  background: var(--primary-light);
}

/* 部分選択時:一括操作バー表示 */
table:has(tbody input:checked) .batch-actions {
  display: flex;
}

/* 未選択時:非表示 */
table:not(:has(tbody input:checked)) .batch-actions {
  display: none;
}

リスト空状態

/* 空リストにプレースホルダー表示 */
.list:has(> :only-child.empty-placeholder) {
  justify-content: center;
  min-height: 200px;
}

:has() と JavaScript の使い分け

シナリオ :has() の利点 JS の利点
DOM 構造変化 自動追従、リスナー不要 非同期データに対応
性能 ネイティブ、スタイル計算が速い 計算タイミングの精密制御
複雑ロジック 単純条件判定 多段計算、API 呼び出し
互換性 2023 年以降フルサポート 互換性の懸念なし

性能上の注意

/* 悪い例:深いネストの :has() は性能に影響 */
.container:has(.deep1:has(.deep2:has(.deep3))) { }

/* 良い例:フラットな :has() 条件 */
.container:has(.deep3) { }

多段ネストの :has() は避けてください。ブラウザは DOM ツリーを走査してマッチングするため、ネストが深いほどオーバーヘッドが増大します。


他の CSS4 機能との組み合わせ

:is() と :where() でセレクターを簡略化

/* :is() で多型チェックを簡略化 */
.card:has(:is(img, video, svg)) {
  aspect-ratio: 16/9;
}

/* :where() で詳細度を下げ、オーバーライドしやすく */
.card:where(:has(.featured)) {
  border-color: gold;
}
/* 開いたモーダルがある時:背景スクロール禁止 */
body:has(:modal) {
  overflow: hidden;
}

body:has([popover]:not(:popover-closed)) {
  overflow: hidden;
}

ベストプラクティス

  • JS のクラス切り替えより :has() を優先:JS コード削減、スタイルロジックの集約
  • :has() 条件はフラットに:ネストで性能劣化を回避
  • 意味的属性と組み合わせaria-current:user-invalid:checked など
  • プログレッシブエンハンスメント:コア機能は :has() に依存しない
  • DevTools でデバッグ:Chrome DevTools は :has() セレクターのハイライトに対応

まとめ

:has() セレクターは CSS 史上最も重要な機能拡張の一つです。子から親への逆方向選択を可能にし、フォームバリデーション、条件スタイル、状態連動などで JavaScript を大幅に削減できます。:has() のマッチングロジックと性能境界を理解することは、モダンフロントエンドエンジニアの必須スキルです。

Flexbox ツール でレイアウト組み合わせを練習、Box Shadow ジェネレーター でカードシャドウ効果を作成、Border Radius ツール でカード角丸をデザインできます。

ブラウザローカルツールを無料で試す →

#:has()#CSS选择器#父选择器#条件样式#CSS4