使用 CSS @scope at-rule 限制選取器的觸及範圍

瞭解如何使用 @scope 僅在 DOM 有限的子樹狀結構內選取元素。

瀏覽器支援

  • 118
  • 118
  • x
  • x

編寫 CSS 選取器的精緻美術

編寫選取器時,你可能會在兩個世界間發現自己的混亂情況。您得特別精確指出該選取哪些元素。另一方面,您希望選取器易於覆寫,且不必與 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-rule 的範圍根。

@scope (.card) {
    img {
        border-color: green;
    }
}

範圍樣式規則 img { … } 只能有效選取位於相符 .card 元素「範圍內」<img> 元素。

如要避免選取資訊卡內容區域 (.card__content) 中的 <img> 元素,您可以讓 img 選取器更明確。另一個做法是使用 @scope 規則是否也接受範圍限制 (也就是決定下限) 的事實。

@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 Nesting 後方加上 & 選取器。

@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 */
  }
  :root :root { /* ❌ 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> 元素的父項元素。

cascade 中的 @scope

CSS Cascade 中,@scope 也新增了一個條件:指定鄰近區域。步驟需等到具體程度之後,到出現順序前。

CSS Cascade 的圖表。

根據規格

比較限定範圍根層級不同在樣式規則中的宣告時,如果宣告範圍根層級和範圍樣式規則主體之間僅有最少代數或同層級元素躍點,其會勝出。

在巢狀結構元件的多個變化版本時,這個新步驟非常實用。以下範例尚未使用 @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 顏色。

(封面相片由 rustam burkhanov 在 Unsplash 上提供)