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

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

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: behind a flag.
  • Safari: 17.4.

Source

撰寫 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 at-rule 也接受範圍限制的事實,這個限制會決定下限。

@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 一次,因為您無法在範圍根目錄中比對範圍根目錄。

@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 也新增了一個條件:範圍相近。該步驟會在特定性之後,但在出現順序之前。

CSS 層級顯示圖。

根據規格

比較樣式規則中不同範圍根目錄的宣告時,如果範圍根目錄和範圍樣式規則主體之間的世代或同胞元素跳躍次數最少,則會採用該宣告。

當您要巢狀化元件的多個變化版本時,這個新步驟就會派上用場。請參考以下尚未使用 @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 上提供)