:has():系列選取器

自從 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() 可帶來許多商機。甚至是尚未發現的內容。請考慮採用其中一些做法。

選取具有直接 figcaptionfigure 元素。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) { … } 選取沒有 figcaptionfigure 後方段落 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-mediacard--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-expandedtrue 時,請使用 :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 一律會為 false css :has(:visited) { … }

如要查看與 :has() 相關的實際成效指標,請參閱這個 Glitch。感謝 Byungwoo 分享這些洞察資料和實作細節。

就是這麼簡單!

準備好迎接 :has()。請告訴親朋好友,並分享這篇文章,這將改變我們處理 CSS 的方式。

所有示範項目皆可在這個 CodePen 集合中找到。