自從 CSS 誕生以來,我們就一直在各方面使用階層式設計。我們的樣式會組成「階層式樣式表」。我們的選取器也會層層相疊。可以橫向發展。在大多數情況下,這些連結會向下移動。但絕不會向上。多年以來,我們一直希望能推出「上層元素選取器」。終於推出了!以 :has()
虛擬選取器的形式。
如果傳遞為參數的任何選取器至少與一個元素相符,:has()
CSS 擬類別就會代表該元素。
但它不只是一個「父項」選取器,這是行銷產品的好方法。不太吸引人的做法可能是使用「條件式環境」選取器。但這並沒有那麼響亮。那麼「family」選取器呢?
瀏覽器支援
在繼續說明之前,我們想先提及瀏覽器支援功能。但還不夠。但距離推出的時間越來越近。目前尚未支援 Firefox,但已納入發展藍圖。但這項功能已在 Safari 中推出,並預計在 Chromium 105 中發布。本文中的所有示範都會說明,如果瀏覽器不支援,該示範將無法運作。
如何使用 :has
但究竟什麼是創新文化?請參考下列 HTML,其中包含兩個同層元素,且具有 everybody
類別。如何選取具有 a-good-time
類別的子項?
<div class="everybody">
<div>
<div class="a-good-time"></div>
</div>
</div>
<div class="everybody"></div>
您可以使用 :has()
搭配下列 CSS 來執行這項操作。
.everybody:has(.a-good-time) {
animation: party 21600s forwards;
}
這會選取 .everybody
的第一個例項,並套用 animation
。
在這個範例中,具有 everybody
類別的元素是目標。條件是具有 a-good-time
類別的子項。
<target>:has(<condition>) { <styles> }
不過,您可以將其應用在更多地方,因為 :has()
可帶來許多商機。甚至是尚未發現的內容。請考慮採用其中一些做法。
選取具有直接 figcaption
的 figure
元素。css
figure:has(> figcaption) { ... }
選取沒有直接 SVG 子項的 anchor
css
a:not(:has(> svg)) { ... }
選取有直接 input
同層兄弟的 label
。側向移動!
css
label:has(+ input) { … }
選取 article
,其中子項 img
沒有 alt
文字
css
article:has(img:not([alt])) { … }
選取 documentElement
,其中 DOM 中存在某些狀態
css
:root:has(.menu-toggle[aria-pressed=”true”]) { … }
選取具有奇數個子項的版面配置容器
css
.container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... }
選取格狀檢視畫面中未經滑鼠游標懸停的所有項目
css
.grid:has(.grid__item:hover) .grid__item:not(:hover) { ... }
選取包含自訂元素 <todo-list>
的容器
css
main:has(todo-list) { ... }
選取段落中具有直接同層 hr
元素的所有單一 a
css
p:has(+ hr) a:only-child { … }
選取符合多個條件的 article
css
article:has(>h1):has(>h2) { … }
混合使用。選取 article
,其中標題後面有副標題
css
article:has(> h1 + h2) { … }
選取互動狀態觸發時的 :root
css
:root:has(a:hover) { … }
選取沒有 figcaption
的 figure
後方段落
css
figure:not(:has(figcaption)) + p { … }
您能想到 :has()
有哪些有趣的用途嗎?有趣的是,這項技術鼓勵您打破心智模型。讓您思考「我能否以不同的方式處理這些樣式?」
範例
讓我們來看看如何使用這項功能。
資訊卡
請參考傳統資訊卡示範。我們可以在資訊卡中顯示任何資訊,例如標題、副標題或媒體。以下是基本資訊卡。
<li class="card">
<h2 class="card__title">
<a href="#">Some Awesome Article</a>
</h2>
<p class="card__blurb">Here's a description for this awesome article.</p>
<small class="card__author">Chrome DevRel</small>
</li>
想介紹媒體內容時,會發生什麼情況?在這個設計中,資訊卡可分成兩個欄。在此之前,您可能會建立新類別來代表這項行為,例如 card--with-media
或 card--two-columns
。這些類別名稱不僅難以聯想,也難以維護及記憶。
您可以使用 :has()
偵測資訊卡是否含有媒體,並採取適當的動作。不需要使用修飾符類別名稱。
<li class="card">
<h2 class="card__title">
<a href="/article.html">Some Awesome Article</a>
</h2>
<p class="card__blurb">Here's a description for this awesome article.</p>
<small class="card__author">Chrome DevRel</small>
<img
class="card__media"
alt=""
width="400"
height="400"
src="./team-awesome.png"
/>
</li>
而且你也不必將其保留在那裡。您可以發揮創意,顯示「精選」內容的資訊卡如何在版面配置中調整?這段 CSS 會讓精選卡片的寬度與版面配置相同,並將其放在格狀區塊的開頭。
.card:has(.card__banner) {
grid-row: 1;
grid-column: 1 / -1;
max-inline-size: 100%;
grid-template-columns: 1fr 1fr;
border-left-width: var(--size-4);
}
如果精選資訊卡含有橫幅,並會因吸引注意而晃動,會發生什麼情況?
<li class="card">
<h2 class="card__title">
<a href="#">Some Awesome Article</a>
</h2>
<p class="card__blurb">Here's a description for this awesome article.</p>
<small class="card__author">Chrome DevRel</small>
<img
class="card__media"
alt=""
width="400"
height="400"
src="./team-awesome.png"
/>
<div class="card__banner"></div>
</li>
.card:has(.card__banner) {
--color: var(--green-3-hsl);
animation: wiggle 6s infinite;
}
無限可能。
表單
那麼表單呢?因為它們很難搭配。舉例來說,您可以為輸入框和標籤套用樣式。我們如何表示欄位有效?有了 :has()
,這項工作就會變得簡單許多。我們可以連結至相關的表單偽類別,例如 :valid
和 :invalid
。
<div class="form-group">
<label for="email" class="form-label">Email</label>
<input
required
type="email"
id="email"
class="form-input"
title="Enter valid email address"
placeholder="Enter valid email address"
/>
</div>
label {
color: var(--color);
}
input {
border: 4px solid var(--color);
}
.form-group:has(:invalid) {
--color: var(--invalid);
}
.form-group:has(:focus) {
--color: var(--focus);
}
.form-group:has(:valid) {
--color: var(--valid);
}
.form-group:has(:placeholder-shown) {
--color: var(--blur);
}
請在這個範例中試試:輸入有效和無效的值,並開啟和關閉焦點。
您也可以使用 :has()
顯示或隱藏欄位的錯誤訊息。請將「email」欄位群組取出,並在其中加入錯誤訊息。
<div class="form-group">
<label for="email" class="form-label">
Email
</label>
<div class="form-group__input">
<input
required
type="email"
id="email"
class="form-input"
title="Enter valid email address"
placeholder="Enter valid email address"
/>
<div class="form-group__error">Enter a valid email address</div>
</div>
</div>
根據預設,您會隱藏錯誤訊息。
.form-group__error {
display: none;
}
但當欄位變成 :invalid
且未聚焦時,您可以顯示訊息,而不需要額外的類別名稱。
.form-group:has(:invalid:not(:focus)) .form-group__error {
display: block;
}
您可以為使用者與表單互動時,加入一些有品味的奇幻元素。請參考以下範例。請注意,您輸入的微互動值必須有效。:invalid
值會導致表單群組震動。但只有在使用者沒有動作偏好設定時才會發生。
內容
我們在程式碼範例中提到了這一點。不過,您如何在文件流程中使用 :has()
?例如,它會提供我們如何為媒體周圍的字體設定樣式。
figure:not(:has(figcaption)) {
float: left;
margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}
figure:has(figcaption) {
width: 100%;
margin: var(--size-fluid-4) 0;
}
figure:has(figcaption) img {
width: 100%;
}
此範例包含圖表。如果沒有 figcaption
,則會在內容中浮動。當 figcaption
出現時,會佔用整個寬度並取得額外的邊距。
回應狀態
如何讓樣式在標記中對某些狀態做出反應?請參考以下「經典」滑動式導覽列的範例。如果您有切換開啟導覽功能的按鈕,可能會使用 aria-expanded
屬性。您可以使用 JavaScript 更新適當的屬性。當 aria-expanded
為 true
時,請使用 :has()
偵測這項情況,並更新滑動式導覽列的樣式。JavaScript 會負責處理相關工作,CSS 則可根據這些資訊執行所需的操作。您不需要重新排列標記或新增額外的類別名稱等 (注意:這不是正式版範例)。
:root:has([aria-expanded="true"]) {
--open: 1;
}
body {
transform: translateX(calc(var(--open, 0) * -200px));
}
:has 是否有助於避免使用者操作錯誤?
這些範例有什麼共通之處?除了顯示使用 :has()
的方式之外,這些方法都不需要修改類別名稱。他們分別插入新內容並更新屬性。這正是 :has()
的一大優點,因為這項功能有助於減少使用者錯誤。有了 :has()
,CSS 就能負責調整 DOM 中的修改內容。您不需要在 JavaScript 中同時處理多個類別名稱,因此開發人員出錯的可能性也會降低。我們都曾經歷過輸入錯誤的類別名稱,並且必須在 Object
查詢中保留這些名稱的情況。
這很有趣,能否讓我們使用更簡潔的標記和更少的程式碼?因為我們不會進行太多 JavaScript 調整,因此 JavaScript 會減少。減少 HTML 的使用量,因為您不再需要 card card--has-media
等類別。
跳脫框架思考
如上所述,:has()
鼓勵您打破心智模型。這是嘗試不同做法的機會。其中一種方法就是嘗試使用 CSS 建立遊戲機制,例如,您可以使用表單和 CSS 建立步驟式機制。
<div class="step">
<label for="step--1">1</label>
<input id="step--1" type="checkbox" />
</div>
<div class="step">
<label for="step--2">2</label>
<input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
--hue: 10;
opacity: 0.2;
}
.step:has(:checked) + .step:not(.step:has(:checked)) {
--hue: 210;
opacity: 1;
}
這也為我們開啟了有趣的可能性。您可以使用該方法,透過轉換來遍歷表單。請注意,這個示範影片最好在獨立的瀏覽器分頁中觀看。
想玩點有趣的遊戲嗎?不妨試試經典的電擊線遊戲。使用 :has()
更容易建立機制。如果游標移到電線上,遊戲就會結束。是的,我們可以使用同層 組合運算子 (+
和 ~
) 等元素建立部分遊戲機制。不過,:has()
可讓您不必使用有趣的標記「技巧」就能達到相同的結果。請注意,這個示範影片最好在獨立的瀏覽器分頁中觀看。
雖然您不會很快將這些內容放入正式版,但這些內容會強調您可以使用原始元素的方式。例如,能夠鏈結 :has()
。
:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
--display-win: 1;
}
效能和限制
離開前,請問您無法使用 :has()
做什麼?:has()
有一些限制。主要原因是效能受影響。
- 您無法
:has()
:has()
。但您可以鏈結:has()
。css :has(.a:has(.b)) { … }
:has()
css :has(::after) { … } :has(::first-letter) { … }
中未使用虛擬元素- 限制在只接受複合選擇器的偽裝中使用
:has()
css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
- 限制在
css ::part(foo):has(:focus) { … }
後使用:has()
- 使用
:visited
一律會為 falsecss :has(:visited) { … }
如要查看與 :has()
相關的實際成效指標,請參閱這個 Glitch。感謝 Byungwoo 分享這些洞察資料和實作細節。
就是這麼簡單!
準備好迎接 :has()
。請告訴親朋好友,並分享這篇文章,這將改變我們處理 CSS 的方式。
所有示範項目皆可在這個 CodePen 集合中找到。