【CSS】:has()疑似クラスの使い方:親・子・孫要素の指定や複数条件との組み合わせ

css-has

:has()は、子要素や兄弟要素の状態に応じて「親要素」を自由にデザインできるセレクタです。

本記事では、基本の書き方からJavaScript不要の動的UI実装、複数条件の組み合わせや効かない時の解決策までを解説します。

また、CSSに関するカテゴリーページから学びたい内容を決めたい人は、以下のCSSページをご確認ください。

目次

:has()擬似クラスとは:親要素を指定できるセレクタ

長年、フロントエンド開発において「子要素の状態に応じて親要素のスタイルを変える」ことは、CSSだけでは不可能とされてきました。

しかし、CSSにおいて革新的なアップデートと言えるのが:has()です。

:has()は、別名「親セレクタ」とも呼ばれる機能です。

ブラウザのレンダリングエンジンで標準有効化されて以来、:has()の使い方をマスターすることは、モダンなWeb制作において必須のスキルとなりました。

ここでは、:has()の実用的なテクニックと注意点を徹底解説します。

:has()擬似クラスとは
  • JavaScript不要!子要素の状態から親要素を指定する
  • 対応ブラウザと使えない環境への代替案

JavaScript不要!子要素の状態から親要素を指定する

例えば、「内部に画像を持つカード要素だけ余白を変える」や「チェックボックスがONになったらカード全体の背景色を変える」といったUIを作る場合、JavaScriptを使って親要素にis-activeなどのクラスを付与するしかありませんでした。

しかし、:has()機能を使えば、JavaScriptは一切不要になります。

親要素:has(子要素)と記述するだけで、「特定の子要素を持つ親」だけを狙い撃ちできます。

実務で:has()を使う場合、条件の範囲(スコープ)を明確にするのがよいです。

カードの「直下」にある画像だけを条件にしたい場合は、.card:has(> img)のように、子コンビネータ(>)や隣接コンビネータ(+)を組み合わせて厳密に指定してください。

これにより、意図しない誤作動を防ぐことができます。

⭕️ :has()を使えば、チェック状態で「親のカード全体」のデザインを変えられる

/* 💡 ベースとなるカードのスタイル */
.plan-card {
  border: 2px solid #dee2e6;
  background-color: #fff;
  transition: all 0.3s ease;
}

/* ⭕️ プロの鉄則:中のラジオボタンが :checked 状態なら、親カードの枠線と背景を変える */
.plan-card:has(.plan-radio:checked) {
  border: 2px solid #0d6efd; /* 💡 青い枠線に変更 */
  background-color: #f0f7ff; /* 💡 青みがかった背景に変更 */
  box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
HTMLコード表示
<div class="has-sec1-wrapper">

  <p class="has-caption">⭕️ :has()を使えば、チェック状態で「親のカード全体」のデザインを変えられる</p>

  <div class="has-demo-area">
    
    <label class="has-plan-card">
      <div class="has-plan-header">
        <input type="radio" name="plan" class="has-plan-radio" checked>
        <span class="has-plan-title">Basic Plan</span>
      </div>
      <p class="has-plan-price">¥1,000 / 月</p>
    </label>

    <label class="has-plan-card">
      <div class="has-plan-header">
        <input type="radio" name="plan" class="has-plan-radio">
        <span class="has-plan-title">Pro Plan</span>
      </div>
      <p class="has-plan-price">¥3,000 / 月</p>
    </label>

  </div>

  <div class="has-code">
    /* 💡 ベースとなるカードのスタイル */<br>
    <span class="hl-blue">.plan-card</span> {<br>
      <span class="hl-green">border: 2px solid #dee2e6;</span><br>
      <span class="hl-green">background-color: #fff;</span><br>
      <span class="hl-green">transition: all 0.3s ease;</span><br>
    }<br><br>

    /* ⭕️ 中のラジオボタンが :checked 状態なら、親カードの枠線と背景を変える */<br>
    <span class="hl-blue">.plan-card:has(.plan-radio:checked)</span> {<br>
      <span class="hl-red">border: 2px solid #0d6efd;</span> /* 💡 青い枠線に変更 */<br>
      <span class="hl-red">background-color: #f0f7ff;</span> /* 💡 青みがかった背景に変更 */<br>
      <span class="hl-red">box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);</span><br>
    }
  </div>

</div>
CSSコード表示
.has-sec1-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-demo-area {
  display: flex;
  gap: 20px;
  justify-content: center;
  background-color: #e9ecef;
  padding: 40px 20px;
  border-radius: 4px;
  border: 1px dashed #adb5bd;
  margin-bottom: 20px;
}

/* 💡 ベースとなるカード要素 */
.has-plan-card {
  display: block;
  width: 160px;
  padding: 15px;
  border: 2px solid #ced4da;
  border-radius: 8px;
  background-color: #fff;
  cursor: pointer;
  /* 戻る時も滑らかにする */
  transition: all 0.3s ease; 
}

.has-plan-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.has-plan-radio {
  margin: 0;
  cursor: pointer;
}

.has-plan-title {
  font-weight: bold;
  font-size: 14px;
  color: #333;
}

.has-plan-price {
  font-size: 12px;
  color: #666;
  margin: 0;
  text-align: right;
}

/* ⭕️ 中のラジオボタンがチェックされたら、親のスタイルを変える */
.has-plan-card:has(.has-plan-radio:checked) {
  border-color: #0d6efd;
  background-color: #f0f7ff;
  box-shadow: 0 4px 10px rgba(13, 110, 253, 0.15);
  /* 少しだけ上に浮かせる */
  transform: translateY(-2px); 
}

/* ついでに、チェックされたカード内のタイトル色も変える(:has()の強力な応用) */
.has-plan-card:has(.has-plan-radio:checked) .has-plan-title {
  color: #0d6efd;
}

.has-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }

対応ブラウザと使えない環境への代替案

便利な機能ですが、実務で導入する際に立ちはだかるのが対応ブラウザの問題です。

検索するとわかりますが、Chrome、Edge、Safariなどのモダンブラウザでは完全サポートされています。

遅れをとっていたFirefoxもバージョン121でデフォルト有効化されました。

しかし、古い端末やOSをアップデートしていないユーザー環境では使えないケースがまだ存在します。

代替アプローチとして、実務では@supports selector(:has(a))を使った「プログレッシブ・エンハンスメント(対応している環境だけリッチにする)」の手法を取ります。

基本となるレイアウトは従来のCSSで構築し、:has()が使えるブラウザに対してのみ、追加の装飾や便利なUIを上書き提供するという守りの姿勢が必須です。

⭕️ @supports を使って非対応ブラウザからレイアウト崩れを守る

/* 💡 ベーススタイル(すべてのブラウザで安全に表示される) */
.btn {
  display: flex;
  align-items: center;
  padding: 10px 20px;
}

/* ⭕️ :has()が使える環境だけ、デザインを最適化する */
@supports selector(:has(a)) {
  /* もしボタンの中にアイコンが含まれていたら、左側の余白を少し調整する */
  .btn:has(.icon) {
    padding-left: 15px; /* 💡 アイコン分バランスをとる */
    background-color: #e7f1ff; /* 💡 特別なボタンとしてハイライト */
  }
}
HTMLコード表示
<div class="has-sec2-wrapper">

  <p class="has-caption">⭕️ @supports を使って非対応ブラウザからレイアウト崩れを守る</p>

  <div class="has-demo-area">
    
    <div class="has-support-box">
      <a href="#" class="has-btn">
        <span class="has-icon">🚀</span>
        ボタンテキスト
      </a>

      <a href="#" class="has-btn">
        ボタンテキスト
      </a>
    </div>

  </div>

  <div class="has-code">
    /* 💡 ベーススタイル(すべてのブラウザで安全に表示される) */<br>
    <span class="hl-blue">.btn</span> {<br>
      <span class="hl-green">display: flex;</span><br>
      <span class="hl-green">align-items: center;</span><br>
      <span class="hl-green">padding: 10px 20px;</span><br>
    }<br><br>

    /* ⭕️ :has()が使える環境だけ、デザインを最適化する */<br>
    <span class="hl-blue">@supports selector(:has(a))</span> {<br>
      <span class="hl-comment">/* もしボタンの中にアイコンが含まれていたら、左側の余白を少し調整する */</span><br>
      <span class="hl-blue">.btn:has(.icon)</span> {<br>
        <span class="hl-red">padding-left: 15px;</span> /* 💡 アイコン分バランスをとる */<br>
        <span class="hl-red">background-color: #e7f1ff;</span> /* 💡 特別なボタンとしてハイライト */<br>
      }<br>
    }
  </div>

</div>
CSSコード表示
.has-sec2-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-support-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 15px;
}

/* 💡 安全なベーススタイル */
.has-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
  background-color: #fff;
  border: 1px solid #adb5bd;
  border-radius: 4px;
  color: #333;
  text-decoration: none;
  font-weight: bold;
  font-size: 14px;
}

/* ⭕️ :has() をサポートしているブラウザのみに適用される処理 */
@supports selector(:has(a)) {
  /* .has-icon を持つ .has-btn だけスタイルを最適化 */
  .has-btn:has(.has-icon) {
    padding-left: 15px; /* アイコンがある分、左のpaddingを少し狭くして視覚的中央に */
    background-color: #f0f7ff;
    border-color: #0d6efd;
    color: #0d6efd;
  }
}

.has-icon {
  font-size: 16px;
}

.hl-comment { color: #6c757d; font-style: italic; }

:has()の基本と応用:子・孫・兄弟要素の指定方法

CSSの:has()セレクタは、単に「特定の子要素を持つ親」を指定するだけではありません。

セレクタの書き方を工夫することで、孫要素まで深い階層を検索したり、CSS単体では不可能だった前の兄弟要素をスタイリングしたり、Webデザインの常識を覆すことができます。

ここでは、実務で頻出する「子孫・兄弟・属性」との組み合わせパターンを解説します。

:has()の基本と応用:子・孫・兄弟要素の指定方法
  • 子要素・孫要素の有無で判定する
  • 特定のクラスや属性を持つ要素の指定
  • 兄弟要素や直後・直前の要素を指定する

子要素・孫要素の有無で判定する

:has()の括弧内に記述するセレクタによって、直下の子要素だけを見るのか、深い孫要素まで探すのかをコントロールできます。

特定の要素を内包する親を判定する際、「検索範囲」の指定方法が重要になります。

:has()を使うときは、検索スコープ(範囲)を極力狭く限定するのがよいです。

直下の要素だけで判定したい場合は、子コンビネータ(>)を使って.card:has(> img)と記述してください。

これにより、意図しない孫要素の干渉を防ぎ、ブラウザの計算パフォーマンスも向上します。

⭕️ 「>」の有無で、判定される範囲(直下か、孫か)が変わる

① 直下に画像あり

IMG

テキスト

② 孫に画像あり

IMG

テキスト

/* ❌ 初心者の書き方(子孫検索):①と②の両方のカードが反応してしまう */
.card:has(.img-dummy) {
  border: 2px solid red;
}

/* ⭕️ プロの鉄則(直下検索):①のカードだけを正確に狙い撃ちする */
.card:has(> .img-dummy) {
  border: 2px solid #0d6efd;
  background-color: #f0f7ff;
}
HTMLコード表示
<div class="has-d-wrapper">
  
  <p class="has-d-caption">⭕️ 「>」の有無で、判定される範囲(直下か、孫か)が変わる</p>

  <div class="has-d-demo-area">
    
    <div class="has-d-box">
      <p class="has-d-label">① 直下に画像あり</p>
      <div class="has-d-card is-direct">
        <div class="has-d-img-dummy">IMG</div>
        <p>テキスト</p>
      </div>
    </div>

    <div class="has-d-box">
      <p class="has-d-label">② 孫に画像あり</p>
      <div class="has-d-card is-grandchild">
        <div class="has-d-inner">
          <div class="has-d-img-dummy" style="width:30px; height:30px;">IMG</div>
        </div>
        <p>テキスト</p>
      </div>
    </div>

  </div>

  <div class="has-d-code">
    /* ❌ 子孫検索:①と②の両方のカードが反応してしまう */<br>
    <span class="hl-blue">.card:has(.img-dummy)</span> {<br>
      <span class="hl-green">border: 2px solid red;</span><br>
    }<br><br>

    /* ⭕️ 直下検索:①のカードだけを正確に狙い撃ちする */<br>
    <span class="hl-blue">.card:has(> .img-dummy)</span> {<br>
      <span class="hl-red">border: 2px solid #0d6efd;</span><br>
      <span class="hl-red">background-color: #f0f7ff;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-d-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-d-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-d-demo-area {
  display: flex;
  gap: 30px;
  justify-content: center;
  background-color: #e9ecef;
  padding: 30px 20px;
  border-radius: 4px;
  border: 1px dashed #adb5bd;
  margin-bottom: 20px;
}

.has-d-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 160px;
}

.has-d-label {
  font-size: 12px;
  font-weight: bold;
  margin-bottom: 15px;
  color: #333;
}

.has-d-card {
  width: 100%;
  padding: 10px;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 8px;
}

.has-d-img-dummy {
  background-color: #6c757d;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: bold;
  border-radius: 4px;
}

/* 💡 パターンAの直下画像 */
.is-direct > .has-d-img-dummy {
  width: 100%;
  height: 60px;
  margin-bottom: 10px;
}

/* 💡 パターンBの内側(孫)画像 */
.has-d-inner {
  padding: 10px;
  background-color: #f8f9fa;
  margin-bottom: 10px;
}

/* ⭕️ 直下に .has-d-img-dummy を持つカードだけをスタイル変更 */
.has-d-card:has(> .has-d-img-dummy) {
  border-color: #0d6efd;
  background-color: #f0f7ff;
}

.has-d-card p {
  margin: 0;
  font-size: 12px;
  color: #333;
  text-align: center;
}

.has-d-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }

特定のクラスや属性を持つ要素の指定

特定のクラス名を持つ要素が中にあるかどうかで、親のレイアウトを切り替えることも可能です。

例えば複数のクラスを持つ条件やクラスを持たない:not()との併用条件も作成できます。

また、クラスだけでなく属性データを利用し、リンク先やID、カスタムデータ属性の有無でスタイルを適用するテクニックは、実務で強力です。

状態管理(エラー、成功、警告など)で親のスタイルを変える場合、JSでクラスをつけ外しするよりも、HTMLのdata-*属性(例:data-status="error")を使用し、CSSで:has([data-status="error"])として検知するのがよいです。

これにより、状態の競合を防ぎ、保守性が向上します。

⭕️ data属性(data-status)を持つ子要素を検知して親をハイライト

  • タスク 1
  • タスク 2 必須
  • タスク 3 完了
/* 💡 通常のリストアイテムのスタイル */
.list-item {
  border: 1px solid #ced4da;
  background-color: #fff;
}

/* ⭕️ プロの鉄則:子要素の [data-status=”error”] を検知して、親のliを赤くする */
.list-item:has([data-status=”error”]) {
  border-color: #dc3545;
  background-color: #fff5f5;
}

/* ⭕️ [data-status=”success”] を検知して、親のliを緑にする */
.list-item:has([data-status=”success”]) {
  border-color: #198754;
  background-color: #f1fcf5;
}
HTMLコード表示
<div class="has-attr-wrapper">
  
  <p class="has-d-caption">⭕️ data属性(data-status)を持つ子要素を検知して親をハイライト</p>

  <div class="has-d-demo-area" style="flex-direction: column; align-items: center;">
    
    <ul class="has-attr-list">
      <li class="has-attr-item">
        <span>タスク 1</span>
      </li>

      <li class="has-attr-item">
        <span>タスク 2</span>
        <span class="has-badge" data-status="error">必須</span>
      </li>

      <li class="has-attr-item">
        <span>タスク 3</span>
        <span class="has-badge" data-status="success">完了</span>
      </li>
    </ul>

  </div>

  <div class="has-d-code">
    /* 💡 通常のリストアイテムのスタイル */<br>
    <span class="hl-blue">.list-item</span> {<br>
      <span class="hl-green">border: 1px solid #ced4da;</span><br>
      <span class="hl-green">background-color: #fff;</span><br>
    }<br><br>

    /* ⭕️ 子要素の [data-status="error"] を検知して、親のliを赤くする */<br>
    <span class="hl-blue">.list-item:has([data-status="error"])</span> {<br>
      <span class="hl-red">border-color: #dc3545;</span><br>
      <span class="hl-red">background-color: #fff5f5;</span><br>
    }<br><br>

    /* ⭕️ [data-status="success"] を検知して、親のliを緑にする */<br>
    <span class="hl-blue">.list-item:has([data-status="success"])</span> {<br>
      <span class="hl-red">border-color: #198754;</span><br>
      <span class="hl-red">background-color: #f1fcf5;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-attr-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-attr-list {
  list-style: none;
  padding: 0;
  margin: 0;
  width: 100%;
  max-width: 300px;
}

.has-attr-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 15px;
  margin-bottom: 10px;
  background-color: #fff;
  border: 1px solid #ced4da;
  border-radius: 6px;
  font-size: 14px;
  font-weight: bold;
  color: #333;
}

.has-badge {
  font-size: 10px;
  padding: 3px 8px;
  border-radius: 12px;
  color: white;
}

/* 属性セレクタ用のスタイル */
.has-badge[data-status="error"] {
  background-color: #dc3545;
}
.has-badge[data-status="success"] {
  background-color: #198754;
}

/* ⭕️ :has() で data属性を検知して親をスタイリング */
.has-attr-item:has([data-status="error"]) {
  border-color: #dc3545;
  background-color: #fff5f5;
  color: #dc3545;
}

.has-attr-item:has([data-status="success"]) {
  border-color: #198754;
  background-color: #f1fcf5;
  color: #198754;
}

兄弟要素や直後・直前の要素を指定する

これまでのCSSでは、隣接兄弟コンビネータ(+)や一般兄弟コンビネータ(~)を使えば、ある要素の次にある兄弟要素は指定できました。

しかし、:has()の登場により、兄弟要素の中でも、これまでは不可能だった「ある要素の『前にある』兄弟要素」の指定が可能になりました。

実務では、このテクニックを:hoverと組み合わせて、「ホバーしている要素の『前』と『次』の要素だけを少し拡大する」といった、MacOSのDockメニューのような連動アニメーションを、JavaScriptを一切使わずに実装するのがトレンドです。

⭕️ :has() を使えば「前の要素」も操作できる(MacOSのDock風エフェクト)

🏠
🔍
📁
💬
⚙️
/* 💡 ベースのアイコン設定 */
.item {
  transform-origin: bottom center;
  /* 💡 transformだけに絞ることでヌルヌル動く */
  transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* 💡 ホバーされている要素自体を一番大きくする */
.item:hover {
  /* 💡 上に少し浮かす(translateY) */
  transform: scale(1.6) translateY(-5px);
}

/* ⭕️ 【次の要素】を操作する */
.item:hover + .item {
  transform: scale(1.25); /* 💡 少しだけ大きく */
}

/* ⭕️ 【前の要素】を操作する */
.item:has(+ .item:hover) {
  transform: scale(1.25); /* 💡 前の要素も少し大きく */
}
HTMLコード表示
<div class="has-sib-wrapper">
  
  <p class="has-d-caption">⭕️ :has() を使えば「前の要素」も操作できる(MacOSのDock風エフェクト)</p>

  <div class="has-d-demo-area" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);">
    
    <div class="has-dock-container">
      <div class="has-dock-item">🏠</div>
      <div class="has-dock-item">🔍</div>
      <div class="has-dock-item">📁</div>
      <div class="has-dock-item">💬</div>
      <div class="has-dock-item">⚙️</div>
    </div>

  </div>

  <div class="has-d-code">
    /* 💡 ベースのアイコン設定 */<br>
    <span class="hl-blue">.item</span> {<br>
      <span class="hl-green">transform-origin: bottom center;</span><br>
      <span class="hl-comment">/* 💡 marginなどは動かさず、transformだけに絞ることでヌルヌル動く */</span><br>
      <span class="hl-green">transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);</span><br>
    }<br><br>

    /* 💡 ホバーされている要素自体を一番大きくする */<br>
    <span class="hl-blue">.item:hover</span> {<br>
      <span class="hl-comment">/* 💡 marginで押し退けるのをやめ、上に少し浮かす(translateY) */</span><br>
      <span class="hl-red">transform: scale(1.6) translateY(-5px);</span><br>
    }<br><br>

    /* ⭕️ 【次の要素】を操作する */<br>
    <span class="hl-blue">.item:hover + .item</span> {<br>
      <span class="hl-red">transform: scale(1.25);</span> /* 💡 少しだけ大きく */<br>
    }<br><br>

    /* ⭕️ 【前の要素】を操作する */<br>
    <span class="hl-blue">.item:has(+ .item:hover)</span> {<br>
      <span class="hl-red">transform: scale(1.25);</span> /* 💡 前の要素も少し大きく */<br>
    }
  </div>

</div>
CSSコード表示
.has-sib-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-d-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-d-demo-area {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 60px 20px;
  border-radius: 8px;
  border: 1px solid #ced4da;
  margin-bottom: 20px;
  overflow: hidden;
}

/* 💡 すりガラス風(グラスモーフィズム)のコンテナ */
.has-dock-container {
  display: flex;
  gap: 15px; 
  padding: 15px 25px;
  background: rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-radius: 24px;
  border: 1px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  align-items: flex-end; 
  height: 80px;
}

/* 💡 アプリ風のアイコン */
.has-dock-item {
  width: 50px;
  height: 50px;
  background: #ffffff;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  cursor: pointer;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
  transform-origin: bottom center;
  /* 💡 transformとbox-shadowだけをアニメーションさせる */
  transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.2s ease;
}

/* 💡 ホバーされている要素自身 */
.has-dock-item:hover {
  /* 💡 scaleで拡大し、さらに少し上に浮かせる */
  transform: scale(1.6) translateY(-5px);
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
  z-index: 10;
}

/* ⭕️ 次の兄弟要素 */
.has-dock-item:hover + .has-dock-item {
  transform: scale(1.25);
  box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
  z-index: 5;
}

/* ⭕️ 前の兄弟要素(:has) */
.has-dock-item:has(+ .has-dock-item:hover) {
  transform: scale(1.25);
  box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
  z-index: 5;
}

/* コードブロック */
.has-d-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }
.hl-comment { color: #6c757d; font-style: italic; }

状態検知と複数条件・:notとの組み合わせ

:has()擬似クラスは、静的なHTML構造の判定だけにとどまりません。

ユーザーの操作による状態の変化をリアルタイムに検知したり、複数の条件を論理演算のように組み合わせたりすることで、JavaScriptの領域だった動的なUI制御をCSSだけで完結できます。

ここでは、フォームの状態に応じたスタイリング、「AかつB」「Aを含まない」といった複雑な条件指定(:notとの組み合わせ)を解説します。

状態検知と複数条件・:notとの組み合わせ
  • フォームのチェックやフォーカスに連動する
  • 複数条件での指定と関数の考え方
  • :notとの組み合わせや:is():where()との違い

フォームのチェックやフォーカスに連動する

ラジオボタンやチェックボックスが選択された時や入力欄にカーソルが当たった時に、親要素であるカードや検索枠全体のデザインを変えるのは、Webデザインにおける頻出パターンです。

checkedfocusを使えば、クリックや入力といったユーザーアクションを起点に、親要素をスタイリングできます。

また、入力不可のdisabledやマウスが乗っているhoverも同様に検知可能です。

実務で役立つのが、フォームが無効化された時のデザイン制御です。

入力欄がdisabledになった際、親のラベル要素全体をグレーアウトさせたい場合、label:has(input:disabled)と指定して親全体のopacityを下げ、pointer-events: none;を付与するのがよいです。

⭕️ :has() を使ったフォーム連動UI(FocusとDisabled)

/* ⭕️ 中のinputがフォーカスされたら、検索窓全体(親)を光らせる */
.search-box:has(.search-input:focus) {
  border-color: #0d6efd;
  box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.25);
}

/* ⭕️ 中のinputがdisabledなら、ラベル全体(親)をグレーアウトする */
.check-label:has(.check-input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
  background-color: #e9ecef;
}
HTMLコード表示
<div class="has-state-wrapper">
  
  <p class="has-caption">⭕️ :has() を使ったフォーム連動UI(FocusとDisabled)</p>

  <div class="has-demo-area" style="flex-direction: column; gap: 30px;">
    
    <div class="has-search-box">
      <span class="has-search-icon">🔍</span>
      <input type="text" placeholder="キーワードを入力..." class="has-search-input">
    </div>

    <label class="has-check-label">
      <input type="checkbox" disabled class="has-check-input">
      <span>このオプションは現在選択できません</span>
    </label>

  </div>

  <div class="has-code">
    /* ⭕️ 中のinputがフォーカスされたら、検索窓全体(親)を光らせる */<br>
    <span class="hl-blue">.search-box:has(.search-input:focus)</span> {<br>
      <span class="hl-red">border-color: #0d6efd;</span><br>
      <span class="hl-red">box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.25);</span><br>
    }<br><br>

    /* ⭕️ 中のinputがdisabledなら、ラベル全体(親)をグレーアウトする */<br>
    <span class="hl-blue">.check-label:has(.check-input:disabled)</span> {<br>
      <span class="hl-red">opacity: 0.5;</span><br>
      <span class="hl-red">cursor: not-allowed;</span><br>
      <span class="hl-red">background-color: #e9ecef;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-state-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-demo-area {
  display: flex;
  align-items: center;
  background-color: #e9ecef;
  padding: 40px 20px;
  border-radius: 4px;
  border: 1px dashed #adb5bd;
  margin-bottom: 20px;
}

/* 💡 検索窓のベース */
.has-search-box {
  display: flex;
  align-items: center;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 25px;
  padding: 5px 15px;
  width: 100%;
  max-width: 300px;
  transition: all 0.3s ease;
}

.has-search-input {
  border: none;
  outline: none;
  padding: 10px;
  width: 100%;
  font-size: 14px;
}

/* ⭕️ Focus連動(親を光らせる) */
.has-search-box:has(.has-search-input:focus) {
  border-color: #0d6efd;
  box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.25);
}

/* 💡 チェックボックスのベース */
.has-check-label {
  display: flex;
  align-items: center;
  gap: 10px;
  background-color: #fff;
  border: 1px solid #ced4da;
  padding: 15px;
  border-radius: 8px;
  width: 100%;
  max-width: 300px;
  font-size: 13px;
  font-weight: bold;
  cursor: pointer;
}

/* ⭕️ Disabled連動(親を無効化デザインにする) */
.has-check-label:has(.has-check-input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
  background-color: #e9ecef;
}

.has-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }

ラジオボタンやチェックボックスの作り方を詳しく知りたい人は以下から一読ください。

複数条件での指定と関数の考え方

:has()の括弧の中には、複数のセレクタを記述できます。

この性質を利用すると、プログラミング言語の論理演算子のように、複数の条件を組み合わせて親要素を絞り込むことができます。

:has()を関数のように見立てて、論理演算(AND / OR)を使い分けるのがよいです。

  • 【ORの指定】.card:has(.a, .b)(括弧内でカンマ区切りにする)
  • 【ANDの指定】.card:has(.a):has(.b):has()自体を複数繋げて連結させる)

⭕️ カンマは「OR(または)」、連結は「AND(かつ)」

TAG A

カード1

TAG B

カード2

TAG A TAG B

カード3

/* 💡【OR(または)】 AかB、どちらかがあれば赤い枠線にする */
.has-logic-card:has(.tag-a, .tag-b) {
  border-color: #dc3545;
}

/* ⭕️【AND(かつ)】 AとB、両方揃っている場合のみ緑にする */
/* ※下に記述しているため、ORの赤い枠線を上書きして緑になります */
.has-logic-card:has(.tag-a):has(.tag-b) {
  border-color: #198754;
  background-color: #f1fcf5;
}
HTMLコード表示
<div class="has-logic-wrapper">
  
  <p class="has-caption">⭕️ カンマは「OR(または)」、連結は「AND(かつ)」</p>

  <div class="has-demo-area" style="gap: 15px; flex-wrap: wrap;">
    
    <div class="has-logic-card">
      <span class="has-tag tag-a">TAG A</span>
      <p class="has-logic-txt">カード1</p>
    </div>

    <div class="has-logic-card">
      <span class="has-tag tag-b">TAG B</span>
      <p class="has-logic-txt">カード2</p>
    </div>

    <div class="has-logic-card">
      <div style="display:flex; gap:5px;">
        <span class="has-tag tag-a">TAG A</span>
        <span class="has-tag tag-b">TAG B</span>
      </div>
      <p class="has-logic-txt">カード3</p>
    </div>

  </div>

  <div class="has-code">
    /* 💡【OR(または)】 AかB、どちらかがあれば赤い枠線にする */<br>
    <span class="hl-blue">.has-logic-card:has(.tag-a, .tag-b)</span> {<br>
      <span class="hl-red">border-color: #dc3545;</span><br>
    }<br><br>

    /* ⭕️【AND(かつ)】 AとB、両方揃っている場合のみ緑にする */<br>
    <span class="hl-comment">/* ※下に記述しているため、ORの赤い枠線を上書きして緑になります */</span><br>
    <span class="hl-blue">.has-logic-card:has(.tag-a):has(.tag-b)</span> {<br>
      <span class="hl-red">border-color: #198754;</span><br>
      <span class="hl-red">background-color: #f1fcf5;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-logic-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-logic-card {
  width: 130px;
  height: 90px;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  transition: all 0.3s ease;
}

.has-tag {
  font-size: 10px;
  font-weight: bold;
  padding: 3px 6px;
  border-radius: 4px;
  color: white;
}
.tag-a { background-color: #0dcaf0; }
.tag-b { background-color: #ffc107; color: #333; }

.has-logic-txt {
  margin: 0;
  font-size: 12px;
  font-weight: bold;
  color: #333;
}

/* ==================================================
   💡 ここから下が、実際に効いている :has() の処理です
   ================================================== */

/* 💡【OR】クラス tag-a または tag-b のどちらかを持っていれば赤枠にする */
/* (結果としてカード1、カード2、カード3すべてが一度赤枠の対象になります) */
.has-logic-card:has(.tag-a, .tag-b) {
  border-color: #dc3545; 
}

/* ⭕️【AND】クラス tag-a と tag-b を "両方" 持っている場合のみ、緑にする */
/* (CSSは下に書いたものが優先されるため、カード3だけが緑に上書きされます) */
.has-logic-card:has(.tag-a):has(.tag-b) {
  border-color: #198754;
  background-color: #f1fcf5;
}

/* コードブロック装飾 */
.has-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }
.hl-comment { color: #6c757d; font-style: italic; }

:notとの組み合わせや:is()・:where()との違い

:has()は、否定の疑似クラス:not()と組み合わせることで、「〜を持たない親要素」を抽出できます。

また、要素をグループ化する:is()とは役割が異なります。

迷った際は、:is()は「要素そのものの複数指定」、:has()は「中身の条件指定」と覚えてください。

特定の要素を持たない親を指定したい場合は、:not()を一番外側に書き、その中に:has()を入れて要素:not(:has(除外したいもの))という構文をルールとして守ってください。

これを活用すると、「まだエラーが解消されていないタスク」や「画像が設定されていない空のプレースホルダー」などをCSSだけで検知し、デザインを切り替えられます。

⭕️ :not(:has(…)) を使って「完了アイコンを持たない」項目だけを自動ハイライト

タスク A(完了済)
タスク B(未完了・要対応!)
タスク C(未完了)
/* ❌ 失敗する書き方(画像以外を持つ要素が全部赤くなる) */
.task-item:has(:not(.icon-done)) {
  background-color: red;
}

/* ⭕️ ✅アイコンを【持っていない】タスクだけを目立たせる */
.task-item:not(:has(.icon-done)) {
  border-left: 4px solid #dc3545; /* 💡 左側に赤い警告線 */
  background-color: #fff5f5;
  color: #dc3545;
  font-weight: bold;
}
HTMLコード表示
<div class="has-not-wrapper">
  
  <p class="has-caption">⭕️ :not(:has(...)) を使って「完了アイコンを持たない」項目だけを自動ハイライト</p>

  <div class="has-demo-area" style="flex-direction: column;">
    
    <div class="has-task-item">
      <span>タスク A(完了済)</span>
      <span class="has-icon-done">✅</span>
    </div>

    <div class="has-task-item">
      <span>タスク B(未完了・要対応!)</span>
      </div>

    <div class="has-task-item">
      <span>タスク C(未完了)</span>
    </div>

  </div>

  <div class="has-code">
    /* ❌ 失敗する書き方(画像以外を持つ要素が全部赤くなる) */<br>
    <span class="hl-blue">.task-item:has(:not(.icon-done))</span> {<br>
      <span class="hl-green">background-color: red;</span><br>
    }<br><br>

    /* ⭕️ ✅アイコンを【持っていない】タスクだけを目立たせる */<br>
    <span class="hl-blue">.task-item:not(:has(.icon-done))</span> {<br>
      <span class="hl-red">border-left: 4px solid #dc3545;</span> /* 💡 左側に赤い警告線 */<br>
      <span class="hl-red">background-color: #fff5f5;</span><br>
      <span class="hl-red">color: #dc3545;</span><br>
      <span class="hl-green">font-weight: bold;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-not-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-demo-area {
  display: flex;
  align-items: stretch;
  background-color: #e9ecef;
  padding: 40px 20px;
  border-radius: 4px;
  border: 1px dashed #adb5bd;
  margin-bottom: 20px;
  gap: 10px;
}

/* 💡 ベースとなるタスクアイテムのスタイル */
.has-task-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background-color: #fff;
  border: 1px solid #ced4da;
  border-radius: 6px;
  font-size: 14px;
  color: #333;
  transition: all 0.3s ease;
}

/* ==================================================
   💡 ここから下が、実際に効いている :not(:has()) の処理です
   ================================================== */

/* ⭕️ ✅アイコン(.has-icon-done)を持たないアイテムだけを自動で警告デザインにする */
.has-task-item:not(:has(.has-icon-done)) {
  border-left: 4px solid #dc3545;
  background-color: #fff5f5;
  color: #dc3545;
  font-weight: bold;
}

/* コードブロック装飾 */
.has-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }

:has()が効かない原因とパフォーマンスの注意点

実務で使い始めると、「なぜか:has()が効かない」という壁にぶつかります。

原因の多くは、DOM(HTMLの構造)とCSSのレンダリングの仕組みに対する理解不足から生じます。

ここでは、「疑似要素やテキスト判定の罠」「非表示要素の仕様」「ネスト(入れ子)のルール」について解説します。

:has()が効かない原因とパフォーマンスの注意点
  • 効かない原因と疑似要素
  • 表示状態による挙動の違い
  • パフォーマンスへの影響とネストのルール

効かない原因と疑似要素

:has()を使って「beforeafterを持つ親を指定したい」と考えたり、「特定の文字が含まれている要素」を指定しようとする初心者が多いです。

しかし、これらは全て機能しません。

:has()はあくまで「HTMLのDOMツリー」の中に存在する要素を探す機能です。

CSSで作られた::before::afterは、HTML上には存在しない「疑似的」な要素であるため:has(::before)と書いても無視されます。

また、タグではなく「ただのテキスト(文字)」も要素ではないため検知できません。

特定の状態や装飾の有無を親に伝えたい場合は、疑似要素やテキスト判定に頼るのではなく、<span><i>などの「実体のあるHTMLタグ」か、「カスタムデータ属性(data-*)」をDOM上に用意して、:has()で検知するのがよいです。

⭕️ :has() はHTML上の「実体」しか見えない(疑似要素は検知不可)

❌ 疑似要素でアイコン
(:hasは反応しない)

テキスト

⭕️ 実体タグでアイコン
(:hasが反応する!)

テキスト

/* ❌ 疑似要素(::before)はDOMにないため検知できない */
.card:has(::before) {
  border: 2px solid red; /* 💡 効かない */
}

/* ❌ 文字列の検知も不可能(:has(“New”)のような書き方はできない) */
.card:has(text) {
  border: 2px solid red; /* 💡 効かない */
}

/* ⭕️ 実体のあるタグ(.real-icon)を用意して検知する */
.card:has(> .real-icon) {
  border: 2px solid #0d6efd;
  background-color: #f0f7ff;
}
HTMLコード表示
<div class="has-trap-wrapper">
  
  <p class="has-caption">⭕️ :has() はHTML上の「実体」しか見えない(疑似要素は検知不可)</p>

  <div class="has-demo-area">
    
    <div class="has-trap-box">
      <p class="has-trap-label">❌ 疑似要素でアイコン<br><small>(:hasは反応しない)</small></p>
      <div class="has-trap-card is-pseudo">
        <p>テキスト</p>
      </div>
    </div>

    <div class="has-trap-box">
      <p class="has-trap-label" style="color:#0d6efd;">⭕️ 実体タグでアイコン<br><small>(:hasが反応する!)</small></p>
      <div class="has-trap-card">
        <span class="has-real-icon">★</span>
        <p>テキスト</p>
      </div>
    </div>

  </div>

  <div class="has-code">
    /* ❌ 疑似要素(::before)はDOMにないため検知できない */<br>
    <span class="hl-blue">.card:has(::before)</span> {<br>
      <span class="hl-green">border: 2px solid red;</span> /* 💡 効かない */<br>
    }<br><br>

    /* ❌ 文字列の検知も不可能(:has("New")のような書き方はできない) */<br>
    <span class="hl-blue">.card:has(text)</span> {<br>
      <span class="hl-green">border: 2px solid red;</span> /* 💡 効かない */<br>
    }<br><br>

    /* ⭕️ 実体のあるタグ(.real-icon)を用意して検知する */<br>
    <span class="hl-blue">.card:has(> .real-icon)</span> {<br>
      <span class="hl-red">border: 2px solid #0d6efd;</span><br>
      <span class="hl-red">background-color: #f0f7ff;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-trap-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-caption {
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 20px;
  color: #198754;
  text-align: center;
}

.has-demo-area {
  display: flex;
  gap: 30px;
  justify-content: center;
  background-color: #e9ecef;
  padding: 40px 20px;
  border-radius: 4px;
  border: 1px dashed #adb5bd;
  margin-bottom: 20px;
}

.has-trap-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 160px;
}

.has-trap-label {
  font-size: 12px;
  font-weight: bold;
  margin-bottom: 15px;
  color: #333;
  text-align: center;
  line-height: 1.4;
}

.has-trap-card {
  width: 100%;
  height: 80px;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
}

/* ❌ 疑似要素でアイコンを追加 */
.is-pseudo::before {
  content: "★";
  position: absolute;
  top: 5px;
  left: 10px;
  color: #6c757d;
}

/* ⭕️ 実体タグでアイコンを追加 */
.has-real-icon {
  position: absolute;
  top: 5px;
  left: 10px;
  color: #0d6efd;
}

/* ❌ :has(::before) は効かないので枠はグレーのまま */
.has-trap-card:has(::before) {
  border-color: red;
}

/* ⭕️ 実体タグを持つカードだけスタイルが変わる */
.has-trap-card:has(> .has-real-icon) {
  border-color: #0d6efd;
  background-color: #f0f7ff;
}

.has-code {
  background-color: #282c34;
  color: #abb2bf;
  padding: 15px;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
  line-height: 1.6;
  border-left: 4px solid #0d6efd;
}

.hl-blue { color: #61afef; font-weight: bold; }
.hl-red { color: #e06c75; font-weight: bold; }
.hl-green { color: #98c379; font-weight: bold; }

実体を持たせるために付与するspanタグの使い方を詳しく知りたい人は「【HTML】spanタグの使い方:divとの違いやCSS装飾・幅が効かない時の対策」を一読ください。

表示状態による挙動の違い

もう一つ、実務で直面するのが「見えない要素」に対する判定です。

「見えている時だけ」親のスタイルを変えたい場合は、子要素の出し入れ自体をJavaScriptで行うか、CSSで完結させたい場合は.card:has(.error-msg:not([style*="display: none"]))のように、非表示状態を明示的に除外する記述が必要です。

※なお、要素がスクロール可能かどうかを:has()だけで動的に判定することは、CSSの仕様では不可能です。

⭕️ display: none は検知される!DOM上の存在有無がすべて

❌ display: none
(見えないのに親が反応する)

カードA

⭕️ タグ自体がない
(親は反応しない)

カードB

/* ❌ display: none で隠していても、タグが存在すれば検知される! */
.card:has(.badge) {
  border: 2px solid #dc3545; /* 💡 見えないのに赤枠になる */
  background-color: #fff5f5;
}

/* ⭕️ 見えている時だけ反応させたいなら、:not() を組み合わせる */
.card:has(.badge:not([style*=”display: none”])) {
  border: 2px solid #0d6efd;
}
HTMLコード表示
<div class="has-disp-wrapper">
  
  <p class="has-caption">⭕️ display: none は検知される!DOM上の存在有無がすべて</p>

  <div class="has-demo-area">
    
    <div class="has-trap-box">
      <p class="has-trap-label">❌ display: none<br><small>(見えないのに親が反応する)</small></p>
      <div class="has-disp-card is-trap">
        <p>カードA</p>
        <span class="has-badge" style="display: none;">NEW</span>
      </div>
    </div>

    <div class="has-trap-box">
      <p class="has-trap-label" style="color:#0d6efd;">⭕️ タグ自体がない<br><small>(親は反応しない)</small></p>
      <div class="has-disp-card">
        <p>カードB</p>
        </div>
    </div>

  </div>

  <div class="has-code">
    /* ❌ display: none で隠していても、タグが存在すれば検知される! */<br>
    <span class="hl-blue">.card:has(.badge)</span> {<br>
      <span class="hl-red">border: 2px solid #dc3545;</span> /* 💡 見えないのに赤枠になる */<br>
      <span class="hl-red">background-color: #fff5f5;</span><br>
    }<br><br>

    /* ⭕️ 見えている時だけ反応させたいなら、:not() を組み合わせる */<br>
    <span class="hl-blue">.card:has(.badge:not([style*="display: none"]))</span> {<br>
      <span class="hl-green">border: 2px solid #0d6efd;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-disp-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-disp-card {
  width: 100%;
  height: 80px;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.has-badge {
  background-color: #ffc107;
  color: #333;
  font-size: 10px;
  padding: 2px 5px;
  border-radius: 3px;
  font-weight: bold;
}

/* ❌ 罠:.badge が display: none でも反応してしまう設定 */
.has-disp-card:has(.has-badge) {
  border-color: #dc3545;
  background-color: #fff5f5;
  color: #dc3545;
}

表示・非表示に関するdisplayプロパティの使い方を詳しく知りたい人は「【html&css】displayの種類は?flexやinline-blockの違い」を一読ください。

パフォーマンスへの影響とネストのルール

「親を遡れるなら、全部:has()で書けばいいのでは?」と考える人がいますが、それはパフォーマンスの観点から危険です。

ブラウザのレンダリングエンジンは、通常「親から子」へスタイルを計算しますが、:has()は「子を全部探してから親に戻る」という重い処理を行います。

モダンブラウザは高度に最適化されていますが、使い方を間違えるとサイト全体がフリーズするほど重くなります。

  • ネストの回避
    入れ子にしたい場合は、コンビネータを使って.card:has(ul > li.active)のように1つの:has()の中で完結させてください。
  • スコープの限定
    body:has(.active)のような広範囲の検索は避け、.container:has(> .active)のように直下(>)に限定してブラウザの計算負荷を最小限に抑えましょう。
  • Web Componentsの壁
    Shadow DOMを使用している場合、:host(:has(...))は使えますが、内部から外側、または外側からスロットの中身を自由に貫通して検知することはできません。
    コンポーネントのカプセル化を意識した設計が必要です。

⭕️ :has() はネスト(入れ子)禁止!コンビネータ(>, 空白)でつなぐのが正解

  • 通常のリスト
  • アクティブなリスト
/* ❌ :has()の中に:has()を書くと、CSS自体がエラーで無効になる */
.card:has(ul:has(.active)) {
  border: 2px solid red; /* 💡 完全に無視される */
}

/* ⭕️ 入れ子にせず、1つの :has() の中に子孫セレクタを書いてまとめる */
.card:has(ul .active) {
  border: 2px solid #0d6efd;
  background-color: #f0f7ff;
}
HTMLコード表示
<div class="has-perf-wrapper">
  
  <p class="has-caption">⭕️ :has() はネスト(入れ子)禁止!コンビネータ(>, 空白)でつなぐのが正解</p>

  <div class="has-demo-area">
    
    <div class="has-trap-box" style="width: 250px;">
      <div class="has-perf-card is-correct">
        <ul style="margin: 0; padding-left: 20px;">
          <li>通常のリスト</li>
          <li class="is-active" style="color: #0d6efd; font-weight: bold;">アクティブなリスト</li>
        </ul>
      </div>
    </div>

  </div>

  <div class="has-code">
    /* ❌ :has()の中に:has()を書くと、CSS自体がエラーで無効になる */<br>
    <span class="hl-blue">.card:has(ul:has(.active))</span> {<br>
      <span class="hl-green">border: 2px solid red;</span> /* 💡 完全に無視される */<br>
    }<br><br>

    /* ⭕️ 入れ子にせず、1つの :has() の中に子孫セレクタを書いてまとめる */<br>
    <span class="hl-blue">.card:has(ul .active)</span> {<br>
      <span class="hl-red">border: 2px solid #0d6efd;</span><br>
      <span class="hl-red">background-color: #f0f7ff;</span><br>
    }
  </div>

</div>
CSSコード表示
.has-perf-wrapper {
  background-color: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.has-perf-card {
  width: 100%;
  padding: 15px;
  background-color: #fff;
  border: 2px solid #ced4da;
  border-radius: 6px;
  font-size: 13px;
}

/* ⭕️ 正しい書き方(ネストさせずに子孫セレクタで書く) */
.has-perf-card:has(ul .is-active) {
  border-color: #0d6efd;
  background-color: #f0f7ff;
}

まとめ

分かりやすいようにまとめを記載します。

本記事のまとめ
  • 子要素や孫要素の有無・状態を条件にして、親要素や前の兄弟要素を指定できる疑似クラス。
  • フォームの:checked:focusと組み合わせることで、JavaScriptなしで動的なUIを実装できる。
  • 意図しない広範囲への適用を防ぐため、直下の子要素を指定する>コンビネータと組み合わせてスコープを絞るのが基本。
  • 括弧内のカンマ区切り(:has(.a, .b))は「OR(または)」を意味する。
  • 複数の:has()を連結させる(:has(.a):has(.b))ことで「AND(かつ)」を表現できる。
  • 「特定の要素を持たない」条件を作る場合は、:not(:has(...))の順番で記述する。
  • 自分の直後に特定の要素があるかを見る:has(+ .target)を使えば、「前の兄弟要素」を操作できる。
  • ::beforeなどの疑似要素や単なるテキスト(文字)を条件として検知することはできない。
  • display: noneで隠れている要素もDOMに存在すれば検知されるため、必要に応じて:not([style*="display: none"])等で除外する。
  • :has()の中に:has()を記述する入れ子は仕様上無効となり、CSS全体がエラーになる。
  • 主要なモダンブラウザでサポート済み。
    非対応環境へは@supports selector(:has(a))を用いて安全にフォールバックを行う。

よくある質問(FAQ)

:has()はすべてのブラウザで使えますか?

現在、Chrome、Edge、Safari、Firefoxなど主要なモダンブラウザの最新バージョンではすべて標準サポートされており、実務で問題なく使用できます。

ただし、Internet Explorerや、アップデートされていない古いスマートフォン環境では動作しません。

そのため、重要なレイアウトや機能が:has()に依存しすぎないよう、非対応ブラウザ向けのベーススタイルを組んだ上で、@supports selector(:has(a))を使って機能を追加するのが安全な設計です。

:has()を多用すると、ページの表示速度が遅くなりませんか?

ブラウザのレンダリングエンジンは :has()を高速に処理できるよう最適化されているため、一般的な使い方であればパフォーマンスが低下することはありません。

ただし、body:has(...)* :has(...)のように、ページ全体から広範囲に検索させるような書き方はブラウザに負荷をかけます。

.card:has(> .icon)のように、対象となる親要素の範囲をできるだけ絞って記述するのがパフォーマンスを保ちます。

:has()と:is()の違いは何ですか?

役割がまったく異なります。

:is()は「セレクタをグループ化して短く書く」ための機能です(例:h1:hover, h2:hover:is(h1, h2):hover と書ける)。

一方、:has()は「特定の中身を持っている親要素自身を指定する」機能です。

用途が違うため、「AかBを持つ親」を指定するために.card:has(:is(.a, .b))のように組み合わせて使うこともあります。

::beforeや::afterなどの疑似要素を:has()で検知することはできますか?

いいえ、できません。

:has(::before)のように記述してもCSSは無視します。

:has()は、HTMLのDOMツリー上に実際に存在している「実体のあるタグ(要素)」しか探すことができません。

CSSで作られた疑似要素や、タグで囲まれていない「単なるテキスト(文字)」は検知できないため、状態を判定したい場合はHTML側に<span>やカスタムデータ属性(data-*)などを付与する必要があります。

:has()を使って「前の兄弟要素」を指定するにはどう書けばいいですか?

「自分の直後に、特定の要素が続いているか」を判定することで、実質的に前の要素を指定できます。

例えば、.box:has(+ .active)と記述すると、「直後に.activeを持つ要素が続いている.box自身(=.activeの直前にある要素)」を指定できます。

これを応用すると、ホバーした要素の「前と次」の要素だけを連動して動かすといったアニメーションが可能になります。

この記事を書いた人

sugiのアバター sugi Site operator

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code&UIコピペサイトCode Stock運営中

目次