:has()
功能性 :has() 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(<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> 元素設定樣式。
<section>
<article class="featured">Featured content</article>
<article>Regular content</article>
</section>
<section>
<article>Regular content</article>
</section>
section:has(.featured) {
border: 2px solid blue;
}
結果
與兄弟組合器一起使用
下面例子中的 :has() 樣式宣告調整了 <h1> 標題後的間距,條件是它後面緊跟著一個 <h2> 標題。
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
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
<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
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() 偽類幫助選擇了任何後面緊跟著(由 + 指示)一個 H2、H3 或 H4 元素的 H1、H2 或 H3 元素,並且 CSS 規則減小了這些 H1、H2 或 H3 元素後面的間距。
這個選擇器也可以寫成
:is(h1, h2, h3):has(+ h2, + h3, + h4) {
margin: 0 0 0.25rem 0;
}
邏輯運算
:has() 關係選擇器可以用來檢查多個特性中是否有一個為真,或者是否所有特性都為真。
透過在 :has() 關係選擇器中使用逗號分隔的值,你正在檢查是否存在任何一個引數。x:has(a, b) 會在後代 a 或 b 存在時為 x 設定樣式。
透過將多個 :has() 關係選擇器連結在一起,你正在檢查是否所有引數都存在。x:has(a):has(b) 會在後代 a 和 b 都存在時為 x 設定樣式。
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() 條件。
/* Avoid anchoring :has() to broad elements */
body:has(.sidebar) {
/* styles */
}
:root:has(.content) {
/* styles */
}
*:has(.item) {
/* styles */
}
相反,應將 :has() 錨定到特定的元素,如 .container 或 .gallery,以縮小範圍並提高效能。
/* 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。
/* May trigger full subtree traversal */
.ancestor:has(.foo) {
/* styles */
}
使用子代或兄弟組合器可以限制內部選擇器的範圍,從而降低 DOM 變更帶來的效能成本。在這個例子中,瀏覽器只需要檢查直接子元素或特定兄弟元素的後代。
/* More constrained - limits traversal */
.ancestor:has(> .foo) {
/* direct child */
}
.ancestor:has(+ .sibling .foo) {
/* descendant of adjacent sibling */
}
某些內部選擇器可能會迫使瀏覽器在每次 DOM 變更時都向上遍歷祖先鏈,以尋找可能需要更新的潛在錨點。當結構暗示需要檢查變更元素的祖先時,就會發生這種情況。
在這個例子中,任何 DOM 變化都需要檢查變更的元素是否為 .foo 的直接子元素(*),以及它的父元素(或更遠的祖先)是否為 .ancestor。
/* Might trigger ancestor traversal */
.ancestor:has(.foo > *) {
/* styles */
}
透過使用特定的類或直接子代組合器(例如,下一個程式碼片段中的 .specific-child)來約束內部選擇器,可以減少昂貴的祖先遍歷,因為它將瀏覽器的檢查限制在一個明確定義的元素上,從而提高效能。
/* Constrain the inner selector to avoid ancestor traversals */
.ancestor:has(.foo > .specific-child) {
/* styles */
}
注意: 隨著瀏覽器對 :has() 實現的最佳化,這些效能特徵可能會得到改善,但基本的約束仍然存在::has() 需要遍歷整個子樹,因此你需要最小化子樹的大小。在像 A:has(B) 這樣的選擇器中,確保你的 A 沒有太多的子元素,並確保你的 B 受到嚴格約束,以避免不必要的遍歷。
規範
| 規範 |
|---|
| 選擇器 Level 4 # 關係型 |
瀏覽器相容性
載入中…