CSS 的 :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 元素,也會間歇性生效。即選取結果未穩定。
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; }
❌ 測試失敗
4️⃣ 實驗四:有圖片的超連結:a:has(img:only-child)
圖片元素因為是替代型元素,所以沒有 :after 及 :before 的偽元素可使用。所以一般為圖片做裝飾的做法是加一個元素包著 img,再在這個父元素設定樣式。有了 :has 選取器,就可以直接選最這些只包著 img 元素的父元素,例如 a:has(img:only-child)
。
例如以下 CSS 樣式設定一個背景紋理,並在滑鼠移入時作背景移動。
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) 成功
🎞 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 樣式直接全域設定不同類別元素位置的作用。
總結,: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 個視頻教學課程。
如果我的文章有價值,請左下角 👍🏻按讚支持,或訂閱贊助我持續創作及分享。