瞭解如何使用 @scope,只選取 DOM 的有限子樹狀結構中的元素。
發布日期:2023 年 10 月 4 日
撰寫選取器時,您可能會發現自己陷入兩難。一方面,您希望選取的元素相當具體,另一方面,您希望選取器仍可輕鬆覆寫,且不會與 DOM 結構緊密耦合。
舉例來說,當您想選取「資訊卡元件內容區域中的主打圖片」時 (這是相當具體的元素選取),您可能不想編寫類似 .card > .content > img.hero 的選取器。
- 這個選取器的特殊性相當高 (
(0,3,1)),因此隨著程式碼成長,會越來越難以覆寫。 - 由於依賴直接子項組合器,因此與 DOM 結構緊密結合。如果標記有所變更,您也需要變更 CSS。
但您也不想只將 img 寫為該元素的選取器,因為這樣會選取整個網頁中的所有圖片元素。
要取得適當的平衡往往相當困難。多年來,部分開發人員已提出解決方案和因應措施,協助您處理這類情況。例如:
- 根據 BEM 等方法,您應為該元素提供
card__img card__img--hero類別,以維持低特異性,同時允許您具體選取內容。 - JavaScript 解決方案 (例如範圍 CSS 或樣式化元件) 會在選取器中加入隨機產生的字串 (例如
sc-596d7e0e-4),藉此重新編寫所有選取器,防止選取器鎖定網頁另一端的元素。 - 有些程式庫甚至完全廢除選取器,要求您直接在標記中放置樣式觸發條件。
但如果不需要這些功能呢?如果 CSS 提供一種方式,讓您能精確選取元素,但不需要編寫高特異性的選取器,或與 DOM 緊密耦合的選取器,會怎麼樣?這時 @scope 就派上用場了,您可以只在 DOM 的子樹狀結構中選取元素。
隆重推出 @scope
使用 @scope 可限制選取器的觸及範圍。方法是設定範圍根,決定要指定目標的子樹狀結構上限。設定範圍根目錄後,所含的樣式規則 (稱為「範圍樣式規則」) 只能從 DOM 的有限子樹狀結構中選取。
舉例來說,如要只以 .card 元件中的 <img> 元素為目標,請將 .card 設為 @scope at 規則的範圍根。
@scope (.card) {
img {
border-color: green;
}
}
範圍樣式規則 img { … } 只能有效選取符合 .card 元素範圍的 <img> 元素。
如要防止選取資訊卡內容區域 (.card__content) 內的 <img> 元素,可以讓 img 選擇器更具體。另一種做法是利用 @scope at 規則也接受 scoping limit 的事實,這會決定下限。
@scope (.card) to (.card__content) {
img {
border-color: green;
}
}
這項範圍樣式規則只會以祖先樹狀結構中 .card 和 .card__content 元素之間的 <img> 元素為目標。這類範圍 (有上下界) 通常稱為「甜甜圈範圍」
:scope 選取器
根據預設,所有範圍樣式規則都與範圍根目錄相關。也可以指定範圍根元素本身。請使用 :scope 選取器。
@scope (.card) {
:scope {
/* Selects the matched .card itself */
}
img {
/* Selects img elements that are a child of .card */
}
}
範圍樣式規則內的選取器會隱含地加上 :scope 前置字元。如要明確指出,可以自行預先加入 :scope。或者,您也可以從 CSS 巢狀結構預先加入 & 選取器。
@scope (.card) {
img {
/* Selects img elements that are a child of .card */
}
:scope img {
/* Also selects img elements that are a child of .card */
}
& img {
/* Also selects img elements that are a child of .card */
}
}
範圍限制可以使用 :scope 虛擬類別,要求與範圍根目錄建立特定關係:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
範圍限制也可以使用 :scope 參照範圍根目錄以外的元素。例如:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
範圍樣式規則本身無法逸出子樹狀結構。:scope + p 等選取項目無效,因為這會嘗試選取範圍外的元素。
@scope 和具體性
您在 @scope 的前奏中使用的選取器不會影響所含選取器的特異性。在本例中,img 選取器的特異性仍為 (0,0,1)。
@scope (#sidebar) {
img { /* Specificity = (0,0,1) */
...
}
}
:scope 的特殊性與一般虛擬類別相同,也就是 (0,1,0)。
@scope (#sidebar) {
:scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
...
}
}
在以下範例中,& 會在內部重新編寫為用於範圍根目錄的選取器,並包裝在 :is() 選取器中。最後,瀏覽器會使用 :is(#sidebar, .card) img 做為選取器進行比對。這個程序稱為「去糖化」。
@scope (#sidebar, .card) {
& img { /* desugars to `:is(#sidebar, .card) img` */
...
}
}
由於 & 是使用 :is() 取消糖化,因此 & 的明確度是按照:is() 明確度規則計算:& 的明確度是其最明確引數的明確度。
以這個範例來說,:is(#sidebar, .card) 的明確度是其最明確引數 (即 #sidebar) 的明確度,因此會變成 (1,0,0)。將此值與 img 的特異性 (即 (0,0,1)) 結合,整個複雜選取器的特異性就會是 (1,0,1)。
@scope (#sidebar, .card) {
& img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
...
}
}
@scope 內的 :scope 和 & 差異
除了特異性計算方式不同之外,:scope 和 & 的另一個差異在於,:scope 代表相符的範圍根,而 & 代表用於比對範圍根的選取器。
因此,& 可以重複使用。這與 :scope 不同,因為您無法在範圍根層級內比對範圍根層級,因此只能使用一次。
@scope (.card) {
& & { /* Selects a `.card` in the matched root .card */
}
:scope :scope { /* ❌ Does not work */
…
}
}
沒有前置範圍
使用 <style> 元素撰寫內嵌樣式時,如未指定任何範圍根,樣式規則的範圍就會限定在 <style> 元素的封閉父項元素。如要這麼做,請省略 @scope 的序曲。
<div class="card">
<div class="card__header">
<style>
@scope {
img {
border-color: green;
}
}
</style>
<h1>Card Title</h1>
<img src="…" height="32" class="hero">
</div>
<div class="card__content">
<p><img src="…" height="32"></p>
</div>
</div>
在上述範例中,範圍規則只會以 div 內的元素為目標,且類別名稱為 card__header,因為該 div 是 <style> 元素的父項元素。
層疊中的 @scope
在 CSS 疊加中,@scope 也會新增一個條件:範圍鄰近程度。這個步驟會在特異性之後,但會在顯示順序之前。
如規格所示:
比較樣式規則中顯示的宣告時,如果範圍根目錄不同,則範圍根目錄與範圍樣式規則主體之間,代間或同層級元素跳轉次數最少的宣告會勝出。
如果巢狀結構中有多個元件變體,這個新步驟就非常實用。請看這個範例,其中尚未用到 @scope:
<style>
.light { background: #ccc; }
.dark { background: #333; }
.light a { color: black; }
.dark a { color: white; }
</style>
<div class="light">
<p><a href="#">What color am I?</a></p>
<div class="dark">
<p><a href="#">What about me?</a></p>
<div class="light">
<p><a href="#">Am I the same as the first?</a></p>
</div>
</div>
</div>
查看該標記的一小部分時,第三個連結會是 white,而不是 black,即使它是套用 .light 類別的 div 子項也一樣。這是因為疊加層會使用顯示順序條件來決定勝出者。系統會發現最後宣告的是 .dark a,因此會根據 .light a 規則勝出
有了範圍鄰近度準則,這個問題現在已解決:
@scope (.light) {
:scope { background: #ccc; }
a { color: black;}
}
@scope (.dark) {
:scope { background: #333; }
a { color: white; }
}
由於兩個範圍限定的 a 選取器具有相同的明確性,因此範圍限定的鄰近條件會開始運作。系統會根據選取器與範圍根的距離,為兩者加權。對於第三個 a 元素,前往 .light 範圍根目錄只需要一個躍點,但前往 .dark 則需要兩個。因此,.light 中的 a 選取器會勝出。
選取器隔離,而非樣式隔離
請注意,@scope 會限制選取器的觸及範圍。不提供樣式隔離功能。繼承至子項的屬性仍會繼承,超出 @scope 的下限。其中一個屬性是 color。在甜甜圈範圍內宣告時,color 仍會向下繼承至甜甜圈洞內的子項。
@scope (.card) to (.card__content) {
:scope {
color: hotpink;
}
}
在這個範例中,.card__content 元素及其子項會繼承 .card 的值,因此顏色為 hotpink。