:has()

Baseline 2023
新推出

自 ⁨2023 年 12 月⁩起,此功能可在最新的裝置和瀏覽器版本上使用。此功能可能無法在較舊的裝置或瀏覽器上使用。

功能性 :has() CSS 偽類表示一個元素,其條件是:作為引數傳遞的相對選擇器在與該元素關聯時,至少匹配一個元素。這個偽類透過接受一個相對選擇器列表作為引數,提供了一種相對於參考元素來選擇父元素或前一個兄弟元素的方法。

css
/* Selects an h1 heading with a
paragraph element that immediately follows
the h1 and applies the style to h1 */
h1:has(+ p) {
  margin-bottom: 0;
}

:has() 偽類的優先順序由其引數中優先順序最高的選擇器決定,與 :is():not() 的方式相同。

語法

css
:has(<relative-selector-list>) {
  /* ... */
}

如果瀏覽器不支援 :has() 偽類本身,那麼整個選擇器塊都將失效,除非 :has() 位於一個寬容選擇器列表(forgiving selector list)中,例如 :is():where()

:has() 偽類不能巢狀在另一個 :has() 中。

偽元素在 :has() 中也不是有效的選擇器,同時偽元素也不是 :has() 的有效錨點。這是因為許多偽元素的存在是基於其祖先元素的樣式來決定的,允許 :has() 查詢它們可能會引入迴圈查詢。

示例

選擇父元素

你可能正在尋找一種“父組合器”,它允許你沿著 DOM 樹向上查詢並選擇特定元素的父元素。:has() 偽類透過使用 parent:has(child)(對於任何父元素)或 parent:has(> child)(對於直接父元素)來實現這一點。這個例子展示瞭如何為一個包含具有 featured 類的子元素的 <section> 元素設定樣式。

html
<section>
  <article class="featured">Featured content</article>
  <article>Regular content</article>
</section>
<section>
  <article>Regular content</article>
</section>
css
section:has(.featured) {
  border: 2px solid blue;
}

結果

與兄弟組合器一起使用

下面例子中的 :has() 樣式宣告調整了 <h1> 標題後的間距,條件是它後面緊跟著一個 <h2> 標題。

HTML

html
<section>
  <article>
    <h1>Morning Times</h1>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
</section>

CSS

css
h1,
h2 {
  margin: 0 0 1rem 0;
}

h1:has(+ h2) {
  margin: 0 0 0.25rem 0;
}

結果

這個例子並排展示了兩個相似的文字進行比較——左邊是一個 H1 標題後跟著一個段落,右邊是一個 H1 標題後跟著一個 H2 標題,然後再跟著一個段落。在右邊的例子中,:has() 幫助選擇了緊跟一個 H2 元素(由相鄰兄弟組合器 + 指示)的 H1 元素,CSS 規則減小了這樣一個 H1 元素後面的間距。如果沒有 :has() 偽類,你無法使用 CSS 選擇器來選擇一個不同型別的前置兄弟元素或父元素。

與 :is() 偽類一起使用

這個例子在之前例子的基礎上,展示瞭如何使用 :has() 選擇多個元素。

HTML

html
<section>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
  <article>
    <h1>Morning Times</h1>
    <h2>Delivering you news every morning</h2>
    <h3>8:00 am</h3>
    <p>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </article>
</section>

CSS

css
h1,
h2,
h3 {
  margin: 0 0 1rem 0;
}

:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
  margin: 0 0 0.25rem 0;
}

結果

這裡,第一個 :is() 偽類用於選擇列表中的任何標題元素。第二個 :is() 偽類用於將一個相鄰兄弟選擇器列表作為引數傳遞給 :has():has() 偽類幫助選擇了任何後面緊跟著(由 + 指示)一個 H2H3H4 元素的 H1H2H3 元素,並且 CSS 規則減小了這些 H1H2H3 元素後面的間距。

這個選擇器也可以寫成

css
:is(h1, h2, h3):has(+ h2, + h3, + h4) {
  margin: 0 0 0.25rem 0;
}

邏輯運算

:has() 關係選擇器可以用來檢查多個特性中是否有一個為真,或者是否所有特性都為真。

透過在 :has() 關係選擇器中使用逗號分隔的值,你正在檢查是否存在任何一個引數。x:has(a, b) 會在後代 ab 存在時為 x 設定樣式。

透過將多個 :has() 關係選擇器連結在一起,你正在檢查是否所有引數都存在。x:has(a):has(b) 會在後代 ab 都存在時為 x 設定樣式。

css
body:has(video, audio) {
  /* styles to apply if the content contains audio OR video */
}
body:has(video):has(audio) {
  /* styles to apply if the content contains both audio AND video */
}

:has() 與正則表示式的類比

有趣的是,我們可以將一些 CSS :has() 結構與正則表示式中的先行斷言(lookahead assertion)聯絡起來,因為它們都允許你根據一個條件來選擇元素(或正則表示式中的字串),而實際上並不選擇匹配該條件的元素(或字串)本身。

正向先行斷言 (?=pattern)

在正則表示式 abc(?=xyz) 中,只有當字串 abc 後面緊跟著字串 xyz 時,abc 才會被匹配。由於這是一個先行斷言操作,xyz 不會包含在匹配結果中。

在 CSS 中類似的結構是 .abc:has(+ .xyz):它僅在存在一個相鄰兄弟元素 .xyz 時才選擇元素 .abc:has(+ .xyz) 部分起到了先行斷言的作用,因為它選擇的是 .abc 元素,而不是 .xyz 元素。

負向先行斷言 (?!pattern)

同樣地,對於負向先行斷言的情況,在正則表示式 abc(?!xyz) 中,只有當字串 abc 後面xyz 時,abc 才會被匹配。類似的 CSS 結構 .abc:has(+ :not(.xyz)) 在下一個元素是 .xyz 時,不會選擇 .abc 元素。

效能注意事項

:has() 偽類的某些用法會顯著影響頁面效能,尤其是在動態更新(DOM 變更)期間。當 DOM 發生變化時,瀏覽器引擎必須重新評估 :has() 選擇器,而複雜或約束不佳的選擇器可能導致昂貴的計算。

避免寬泛的錨點

錨點選擇器(A:has(B) 中的 A)不應該是擁有過多子元素的元素,例如 body:root*。將 :has() 錨定到非常通用的選擇器會降低效能,因為在廣泛選擇的元素的整個子樹中,任何 DOM 變化都需要瀏覽器重新檢查 :has() 條件。

css
/* Avoid anchoring :has() to broad elements */
body:has(.sidebar) {
  /* styles */
}
:root:has(.content) {
  /* styles */
}
*:has(.item) {
  /* styles */
}

相反,應將 :has() 錨定到特定的元素,如 .container.gallery,以縮小範圍並提高效能。

css
/* Use specific containers to limit scope */
.container:has(.sidebar-expanded) {
  /* styles */
}
.content-wrapper:has(> article[data-priority="high"]) {
  /* styles */
}
.gallery:has(> img[data-loaded="false"]) {
  /* styles */
}

最小化子樹遍歷

內部選擇器(A:has(B) 中的 B)應該使用像 >+ 這樣的組合器來限制遍歷。當 :has() 內部的選擇器沒有被嚴格約束時,瀏覽器可能需要在每次 DOM 變更時遍歷錨點元素的整個子樹,以檢查條件是否仍然成立。

在這個例子中,.ancestor 內部的任何變化都需要檢查所有後代元素是否為 .foo

css
/* May trigger full subtree traversal */
.ancestor:has(.foo) {
  /* styles */
}

使用子代或兄弟組合器可以限制內部選擇器的範圍,從而降低 DOM 變更帶來的效能成本。在這個例子中,瀏覽器只需要檢查直接子元素或特定兄弟元素的後代。

css
/* More constrained - limits traversal */
.ancestor:has(> .foo) {
  /* direct child */
}
.ancestor:has(+ .sibling .foo) {
  /* descendant of adjacent sibling */
}

某些內部選擇器可能會迫使瀏覽器在每次 DOM 變更時都向上遍歷祖先鏈,以尋找可能需要更新的潛在錨點。當結構暗示需要檢查變更元素的祖先時,就會發生這種情況。

在這個例子中,任何 DOM 變化都需要檢查變更的元素是否為 .foo 的直接子元素(*),以及它的父元素(或更遠的祖先)是否為 .ancestor

css
/* Might trigger ancestor traversal */
.ancestor:has(.foo > *) {
  /* styles */
}

透過使用特定的類或直接子代組合器(例如,下一個程式碼片段中的 .specific-child)來約束內部選擇器,可以減少昂貴的祖先遍歷,因為它將瀏覽器的檢查限制在一個明確定義的元素上,從而提高效能。

css
/* Constrain the inner selector to avoid ancestor traversals */
.ancestor:has(.foo > .specific-child) {
  /* styles */
}

注意: 隨著瀏覽器對 :has() 實現的最佳化,這些效能特徵可能會得到改善,但基本的約束仍然存在::has() 需要遍歷整個子樹,因此你需要最小化子樹的大小。在像 A:has(B) 這樣的選擇器中,確保你的 A 沒有太多的子元素,並確保你的 B 受到嚴格約束,以避免不必要的遍歷。

規範

規範
選擇器 Level 4
# 關係型

瀏覽器相容性

另見