Makzan
Makzan

我管理世界職業技能競賽之網站技術項目、舉辦本地設計與開發賽事、開課分享技術心得。一個用網頁來表達自己的作家。

CSS 的 :has 選取器介紹

過去三十年,CSS 選取器一直都是向後或向下尋找的,即所有選取器條件,都是應用到條件最後的元素上。這是配合瀏覽器邊下載邊渲染元素的做法,瀏覽器可以在未載入之後的元素時,便已可以應用 CSS 樣式。而 :has 選取器則是應用到選取條件中中間的元素,因而 :has 選取器又被稱為父類選取器。

今日為大家介紹 CSS 的一個新成員— :has 選取器。

:has 選取器自 2015 年起已加入至新 CSS 標準的草稿,並經過多年討論,這個選取器剛剛在 Safari 的技術預覽(Technical Preview)版本 137 實現了。 而其他瀏覽器則仍未推出。

過去三十年,CSS 選取器一直都是向後或向下尋找的,即所有選取器條件,都是應用到條件最後的元素上。這是配合瀏覽器邊下載邊渲染元素的做法,瀏覽器可以在未載入之後的元素時,便已可以應用 CSS 樣式。而 :has 選取器則是應用到選取條件中中間的元素,因而 :has 選取器又被稱為父類選取器。

可以想像,這個選取器和之前一直使用的選取器邏輯算法都不同,這也是為甚麼這麼多年討論,還未大規模落實的原因之一。但通過是次技術預覽版本實現,我們可以先了解這個選取器的應用,實驗一下可以引申出哪些新 CSS 用法,及我如何在 Slides.com 的簡報中使用這個 :has 選取器。

👨🏻‍🏫 :has 選取器介紹

那 :has 選取器是長甚麼樣子的?類似這樣:

有直接 img 元素的 a 元素

a:has(>img)

或是只有一個 img 元素的 a 元素

a:has(img:only-child)

或是配合 :checked:invalid 等狀態選取器,對包著 input 輸入框的元素進行樣式設定。

label:has(:invalid)

🔬 :has 選取器實驗

我做了幾個實驗例子,實驗各種 :has 選取器的穩定性。


1️⃣ 實驗一:只選擇有 figcatpion 的 figure:figure:has(figcaption)

HTML

<section id="demo-1">
  <figure>
    <img src="https://placekitten.com/300/300" alt="Placeholder">
    <figcaption>Figcaption</figcaption>
  </figure>

  <figure>
    <img src="https://placekitten.com/300/300" alt="Placeholder">
  </figure>
</section>

CSS

figure {
  border: 3px solid #fcfcfc;
}
figure:has(figcaption) {
  border: 3px solid lightgrey;
  background: lightgrey;
  display: inline-block;
  text-align: center;
  padding: .5em;
  border-radius: 5px;
}

❌ 測試失敗,連沒有 figcaption 的 figure 元素,也會間歇性生效。即選取結果未穩定。

當有 figcaption 元素時嘗試套用特有樣式。



2️⃣ 實驗二:數量選取器: ul:has(li:nth-child(4))

這個選取器如成功將會很實用。可以用在導航列、新聞列表等容器上,並按其中有多少子元素而決定不同的排版。

HTML

<section id="demo-5">
  <p>When there are 3 or less children:</p>
  <ul>
    <li>A</li>
    <li>B</li>
    <li>C</li>
  </ul>
  <p>When there are more than 3 children, layout changes to 50%, 50% split:</p>
  <p>✅ Working as expected.</p>
  <ul>
    <li>A</li>
    <li>B</li>
    <li>C</li>
    <li>D</li>
    <li>E</li>
    <li>F</li>
  </ul>
</section>

CSS

#demo-5 ul {
  list-style: none;
  margin: 1em 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}
#demo-5 li {
  background: lightgrey;
  padding: .5em;
  border-bottom: 2px solid darkgrey;
}
#demo-5 ul:has(li:nth-child(4)) {
  grid-template-columns: 1fr 1fr;
}

✅ 成功

當子元素有四個或以上時,自動變為另一種排版方式。



3️⃣ 實驗三::has(:invalid) 及 :has(:checked)

這個實驗測試是否可以按 :invalid, :checked 等輸入框狀態,然後對包著這輸入框的 label 元素設定樣式。可惜,實驗結果未能成功,當輸入框的狀態改變時,這個選取器未有更新,但在測試中有曾經成功選擇過,估計是負責更新的算法尚待改善。

將來此用法若成熟,可以引申出不同的用法,例如只有最少選取任何勾選框,提交按鈕才顯示,或有不同的狀態時有更豐富的顯示效果,而不是只局限於輸入框或輸入框後的元素(+寫法)等。

HTML

<section id="demo-3">
  <h2>:has(:invalid)</h2>
  <p>
    <label>
      Any numbers here:
      <input type="text" pattern="\d*">
    </label>
  </p>
  <p>
    <label>
      YYYY here:
      <input type="text" pattern="\d{4}">
    </label>
  </p>  
</section>
<section id="demo-4">
  <h2>:has(:checked)</h2>
  <p>
    <label>
      <input type="radio" name="gender">
      Male
    </label>
  </p>
  <p>
    <label>
      <input type="radio" name="gender">
      Female
    </label> 
  </p> 
</section>

CSS

#demo-3 label:has(:invalid) {
  border-left-color: red;
}

#demo-4 label:has(:checked) {
    border-left-color: green;
}

❌ 測試失敗

:has(:invalid), :has(:checked) 不穩定,大部份時候不成功,此為偶有成功的截圖。

4️⃣ 實驗四:有圖片的超連結:a:has(img:only-child)

圖片元素因為是替代型元素,所以沒有 :after 及 :before 的偽元素可使用。所以一般為圖片做裝飾的做法是加一個元素包著 img,再在這個父元素設定樣式。有了 :has 選取器,就可以直接選最這些只包著 img 元素的父元素,例如 a:has(img:only-child)

例如以下 CSS 樣式設定一個背景紋理,並在滑鼠移入時作背景移動。

a:has(img:only-child) + 偽元素配合 :hover 效果

HTML:

<a href="#">
    <img src="https://placekitten.com/300/300" alt="Placeholder">
</a>

CSS:

/* 選取只有一個 img 元素的 a 元素 */
a:has(img:only-child) {
  display:  inline-block;
  position: relative;
}
/* 移除 inline 圖片下方會有些少空白的問題 */
a:has(img:only-child) img {
  display: block;
}
/* 設定 :before, :after 偽元素基本樣式 */
a:has(img:only-child):before,
a:has(img:only-child):after {
  content:  '';
  position:  absolute;
  width:  100%;
  height: 100%;
  background-size:  10px 10px;
  z-index: -1;
}
/* 設定 :before 偽元素背景紋理,但位置為 0,0 故未可見。 */
a:has(img:only-child):before {
  top: 0;
  left: 0;
  background-image: radial-gradient(lightblue 60%, transparent 60%);
}
/* 設定 :after 偽元素背景紋理 */
a:has(img:only-child):after {
  bottom:  -10px;
  right:  -10px;
  background-image: radial-gradient(steelblue 60%, transparent 60%);
  z-index: -1;
}
/* 設定滑鼠移入時的 :before, :after 偽元素的新位置 */
a:has(img:only-child):hover:before {
  top: -10px;
  left: -10px;
}
a:has(img:only-child):hover:after {
  bottom: -20px;
  right: -20px;
}

✅ a:has(img:only-child) 成功

a:has(img:only-child) 配合 :before, :after 偽元素畫背景裝飾。

🎞 Slides.com 自定義 CSS 樣式中使用 :has 選取器

我平常製作簡報,會使用 Slides.com,當中可以允許我自定義客製 CSS 樣式。也可以讓我為簡報上的物作元素加入 class 類別名稱。但由於其背後的 reveal.js 結構,使我若想使用 CSS 樣式統一不同類別的物件,就必須為每一個物件加入類別名稱,對大型簡報製作尤顯費時。

以下為 reveal.js 的簡報元素結構範例。

<section class="present" style="display: block;">
    <div 
        class="sl-block" 
        data-block-type="text" 
        style="width: 960px; left: 0px; top: 0px; height: auto;" 
    >
        <div 
            class="sl-block-content" 
            data-placeholder-tag="h1" 
            data-placeholder-text="Title Text" 
            style="z-index: 11;"
        >
            <h1>And make ".bg" block full width (as a bg)</h1>
        </div>
    </div>
    <div 
        class="sl-block" 
        data-block-type="shape" 
        style="width: 300px; height: 300px; left: 330px; top: 200px;"
    >
        <div 
            class="sl-block-content bg" 
            data-shape-type="symbol-smiley" 
            data-shape-fill-color="rgb(217, 234, 211)" 
            data-shape-stretch="false" 
            style="z-index: 10;"
        >
            <svg>...</svg>
        </div>
    </div>
</section>

從上述的結構中可以看出,簡報中的每個物件元素,共有三層 DIV,最出面的 .sl-block 決定在簡報中的位置,中間的 .sl-block-content 是自定義 class 類別名稱的設定位置,上方的 "bg" 是自定義的。而第三層則是內容本身。

我一直希望可以統一限制所有相同類型元素的位置,就像套用範本般,例如我希望 h1 都是垂直水平置中,所有的 bg 類別佔據全畫面及 -1 zindex 等。但由於決定位置及尺寸的是最上層的 div,所以唯有使用 :has 選取器來解決。

所以,當我加入以下 CSS 後,就可以設定 h1 及有 .bg 類別的元素位置了。

.sl-block:has(.bg) {
  top: 0 !important;
  left: 0 !important;
  width: 100% !important;
  height: 100% !important;  
}
.sl-block:has(.center),
.sl-block:has(h1) {
  
  top: 0 !important;
  left: 0 !important;
  width: 100% !important;
  height: 100% !important;  
  
  .sl-block-content {
    display: grid;
    place-items: center;
  }
}

然而,現在上述 CSS 樣式只在 Safari 技術預覽版本中能運作,真的有用嗎?有。我拍攝一些教學片時,就會使用 slides.com 預先製作好簡報,再投影錄製成教學片,所以這些簡報的作用是拍攝道具之一,而不是用作分享的,故我自己使用的瀏覽器能支援便足夠,而且拍攝的簡報通常會快速製作,這更突顯套用 CSS 樣式直接全域設定不同類別元素位置的作用。

slides.com 簡報,強制所有 h1 置中,所有 .bg 自動填滿及放到後面。

總結,:has 選取器剛剛在一個測試版瀏覽器上實作了,雖然距離全面使用估計還有段時間,起碼一年半載,但現時已經可以實驗性測試及配合 @supports 使用,兼且在特定場景,還真可以投入生產使用呢。

附上配合 @supports 使用的方法:

@supports selector(a:has(img)) {
  .support{display: block;}
  .not.support{display: none;}
}

上述實驗源代碼:

https://codepen.io/makzan/pen/GRMyzxQ

— 麥誠 Makzan,2021-12-29。


我是麥誠軒(Makzan),除了正職外,平常我要麼辦本地賽與辦世界賽,要麼任教編程與網站開發的在職培訓。現正轉型將面授培訓內容寫成電子書、網上教材等,至今撰寫了 7 本書, 2 個視頻教學課程。

如果我的文章有價值,請左下角 👍🏻按讚支持,或訂閱贊助我持續創作及分享。


麥誠 Makzan
CC BY-NC-ND 2.0 版权声明

喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。

加载中…
加载中…

发布评论