:has():系列選取器

自從我們開始 (CSS 術語) 起,我們一直與串聯式各種情境合作。我們的樣式組成了「階層式樣式表」。我們的選取器也會串聯起來。讓它們傾斜。在大多數情況下,按鈕高度會下降。但從來不會向上。多年來,我們打造了「家長選取器」這個概念。現在終於到了!在 :has() 虛擬選取器的形狀中。

如果做為參數傳遞的任何選取器符合至少一個元素,:has() CSS 虛擬類別代表元素。

但這其實不是「父項」選取器,這就是 Google Play 遊戲的行銷技巧最吸引人的方式可能是「條件環境」選取器,但那個圓環聽起來不太一樣。要如何使用「闔家適用」選取器?

瀏覽器支援

在深入說明之前,我想先介紹瀏覽器支援功能。它還不算什麼。但我現在就快完成了。目前尚不支援任何 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)) { ... } 選取直接同層級 inputlabel。方向不對! css label:has(+ input) { … } 選取子元素「img」不含 alt 文字的「articlecss article:has(img:not([alt])) { … } 選取在 DOM 中顯示某些狀態的 documentElement css :root:has(.menu-toggle[aria-pressed=”true”]) { … } 選取格線中未懸停的所有項目 css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } 選取所有符合自訂元素之容器的容器<todo-list> css main:has(todo-list) { ... } 選取同時符合自訂元素之位置的容器<todo-list> css main:has(todo-list) { ... } 選取 3 個直接元素符合的 {11/1} articlecss .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... }ahrcss p:has(+ hr) a:only-child { … }css article:has(>h1):has(>h2) { … }選取標題後面加上標題的 article css article:has(> h1 + h2) { … } 在觸發互動狀態時選取「:rootcss :root:has(a:hover) { … } 選取 figure 後方不含 figcaption 的段落 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;
}

無限想像可能

表單

表單有何功用?以不容易設計樣式而聞名。例如樣式輸入內容及其標籤就屬於這類情形。舉例來說,Google 如何表明欄位有效?有了 :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() 顯示及隱藏欄位的錯誤訊息。輸入「電子郵件」欄位群組並新增錯誤訊息。

<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() 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) { … } 內不得使用虛擬元素
  • 限制在只接受複合選取器 css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … } 的虛擬環境中使用 :has()
  • 限制在 css ::part(foo):has(:focus) { … } 虛擬元素之後使用 :has()
  • 使用 :visited 一律為 false css :has(:visited) { … }

如要瞭解與 :has() 相關的實際成效指標,請參閱這個 Glitch。感謝 Byungwoo 分享這些深入分析資訊和實作相關詳細資訊。

就是這麼簡單!

準備好迎接 :has()。向朋友介紹並分享這篇文章,你的 CSS 發展策略將大有斬獲。

所有示範內容可在這個 CodePen 衝突中取得。