使用 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 也接受了決定下限的範圍限制

@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> 元素的父項元素。

Cascade 中的 @scope

CSS Cascade 中,@scope 還會加入新條件:指定鄰近區域。步驟必須具有明確性,但在出現順序之前。

CSS Cascade 以視覺化方式呈現。

根據規格

比較在樣式規則中出現的不同範圍根層級宣告的宣告時,則在範圍 Root 與限定範圍樣式規則之間,宣告的代別或同層元素躍點最少就會獲勝。

這個新步驟可在建立元件的多個變化版本巢狀結構時派上用場。以這個範例來說,還沒使用 @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 上提供)