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

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

瀏覽器支援

  • Chrome:118。
  • Edge:118。
  • Firefox:在標記後方。
  • Safari:17.4。

資料來源

撰寫 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 規則的範圍根。

@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 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 上提供)