在容器查詢 polyfill 中

Gerald Monaco
Gerald Monaco

容器查詢是 CSS 的新功能,可讓您編寫樣式邏輯,以便針對父項元素的功能 (例如寬度或高度) 設定子項樣式。近期,polyfill重大更新已發布,同時也已在瀏覽器中提供支援。

在本篇文章中,您將能一窺 polyfill 的運作方式、克服的挑戰,以及使用時的最佳做法,為訪客提供絕佳的使用者體驗。

深入解析

轉譯

當瀏覽器中的 CSS 剖析器遇到不明的 at-rule (例如全新的 @container 規則) 時,會將其捨棄,就好像該規則從未存在一樣。因此,polyfill 必須要做的首要且最重要的事,就是將 @container 查詢轉譯成不會遭到捨棄的內容。

轉譯的第一步是將頂層 @container 規則轉換為 @media 查詢。這可確保內容保持在同一個群組中。例如使用 CSSOM API 和查看 CSS 來源時。

之前
@container (width > 300px) {
  /* content */
}
使用後
@media all {
  /* content */
}

在容器查詢問世之前,CSS 並未提供作者任意啟用或停用一組規則的方法。如要為這項行為進行半透明轉譯,還必須轉換容器查詢中的規則。每個 @container 都會獲得專屬 ID (例如 123),用於轉換各個選取器,讓選取器只在元素具有包含此 ID 的 cq-XYZ 屬性時才套用。這個屬性會在執行階段由 polyfill 設定。

之前
@container (width > 300px) {
  .card {
    /* ... */
  }
}
使用後
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

請注意,使用 :where(...) 虛擬類別。通常,加入額外的屬性選取器會提高選取器的特異性。使用擬似類別時,您可以套用額外條件,同時保留原始的特定性。如要瞭解這項操作的重要性,請參考以下範例:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

根據這段 CSS,具有 .card 類別的元素一律應具有 color: red,因為後者規則一律會使用相同的選擇器和特定性覆寫前者規則。因此,轉譯第一個規則並加入額外的屬性選取器 (不含 :where(...)) 會提高特異性,並導致 color: blue 錯誤套用。

不過,:where(...) 擬群組屬性相當新穎。對於不支援 polyfill 的瀏覽器,polyfill 提供了安全又簡單的解決方法:您可以刻意@container 規則中手動新增虛擬 :not(.container-query-polyfill) 選取器,藉此提高規則的專屬性:

之前
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
使用後
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

這麼做有許多好處:

  • 來源 CSS 中的選取器已變更,因此可以明確看到特異性的差異。這也能做為說明文件,讓您在不再需要支援因應做法或 polyfill 時,知道哪些項目會受到影響。
  • 規則的特定性一律相同,因為 polyfill 不會變更。

在轉譯期間,polyfill 會將這個虛設值替換為具有相同特異性的屬性選取器。為避免發生任何意外,polyfill 會同時使用兩種選取器:原始來源選取器用於判斷元素是否應接收 polyfill 屬性,而轉譯的選取器則用於樣式設定。

擬元素

您可能會問:如果 polyfill 在元素上設定部分 cq-XYZ 屬性,以便納入專屬容器 ID 123,那麼如何支援無法設定屬性的擬造元素?

擬造元素一律會繫結至 DOM 中的實際元素,稱為「原始元素」。在轉譯期間,系統會改為將條件式選取器套用至這個實際元素:

之前
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
使用後
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

條件式選取器不會轉換為 #foo::before:where([cq-XYZ~="123"]) (這會導致無效),而是會移至原始元素 #foo 的結尾。

不過,這並非必要條件。容器不得修改「不在」內部的任何內容 (且容器不能位於自身內部),但如果 #foo 本身就是要查詢的容器元素,就會發生這種情況。#foo[cq-XYZ] 屬性會誤用變更,且任何 #foo 規則都會誤用套用。

為修正這個問題,polyfill 實際上會使用兩個屬性:一個只能由父項套用至元素,另一個則可由元素套用至自身。後者屬性用於指定偽元素的選取器。

之前
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
使用後
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

由於容器絕不會將第一個屬性 (cq-XYZ-A) 套用至自身,因此只有在不同的父項容器符合容器條件並套用時,第一個選取器才會進行比對。

容器相對單位

容器查詢也提供幾個新的單位,可用於 CSS,例如 cqwcqh,分別代表最接近的適當上層容器的寬度和高度的 1%。為支援這些功能,系統會使用 CSS 自訂屬性將單位轉換為 calc(...) 運算式。polyfill 會透過容器元素內嵌樣式設定這些屬性的值。

之前
.card {
  width: 10cqw;
  height: 10cqh;
}
使用後
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

還有邏輯單元,例如 cqicqb,分別用於內嵌大小和區塊大小。這項作業稍微複雜一些,因為內嵌和區塊軸會由使用單位的元素的 writing-mode 決定,而非要查詢的元素。為支援這項功能,polyfill 會將內嵌樣式套用至 writing-mode 與其父項不同的任何元素。

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

單位現在可以轉換為適當的 CSS 自訂屬性,就像先前一樣。

屬性

容器查詢也新增了幾個 CSS 屬性,例如 container-typecontainer-name。由於 getComputedStyle(...) 等 API 無法搭配未知或無效的屬性使用,因此這些屬性也會在剖析後轉換為 CSS 自訂屬性。如果無法剖析屬性 (例如因為屬性含有無效或不明的值),則會讓瀏覽器自行處理。

之前
.card {
  container-name: card-container;
  container-type: inline-size;
}
使用後
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

這些屬性會在偵測到時轉換,讓 polyfill 能與 @supports 等其他 CSS 功能搭配使用。這項功能是使用 polyfill 的最佳做法基礎,詳情請參閱下文。

之前
@supports (container-type: inline-size) {
  /* ... */
}
使用後
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

根據預設,CSS 自訂屬性會沿用,舉例來說,.card 的任何子項都會採用 --cq-XYZ-container-name--cq-XYZ-container-type 的值。這絕對不是原生資源的行為。為解決這個問題,polyfill 會在任何使用者樣式之前插入下列規則,確保每個元素都會收到初始值,除非另有規則刻意覆寫。

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

最佳做法

雖然大多數訪客都會使用內建容器查詢支援功能的瀏覽器,但仍應為其他訪客提供良好的使用體驗。

在初始載入期間,polyfill 必須先完成許多工作,才能為頁面建立版面配置:

  • 需要載入並初始化 polyfill。
  • 樣式表需要進行剖析和轉譯。由於沒有任何 API 可存取外部樣式表的原始來源,因此可能需要非同步重新擷取,但最好只從瀏覽器快取中擷取。

如果 polyfill 未仔細解決這些問題,可能會導致 Core Web Vitals 退步。

為了讓您更輕鬆地為訪客提供良好體驗,polyfill 的設計會優先處理首次輸入延遲 (FID)累積式版面配置偏移 (CLS),但可能會犧牲最大內容繪製 (LCP)。具體來說,polyfill 無法保證容器查詢會在首次顯示畫面前評估。也就是說,為了提供最佳使用者體驗,您必須確保任何會因使用容器查詢而受到大小或位置影響的內容,在 polyfill 載入並轉譯 CSS 之前都處於隱藏狀態。其中一種做法就是使用 @supports 規則:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

建議您將這項功能與純 CSS 載入動畫結合,並將其絕對定位在 (隱藏) 內容上,以便告知訪客發生了什麼事。如需這項方法的完整示範,請參閱這篇文章

我們建議採用這種做法,原因如下:

  • 純 CSS 載入器可為使用較新版瀏覽器的使用者減少額外負擔,同時為使用舊版瀏覽器和較慢網路的使用者提供輕量化意見回饋。
  • 將載入器的絕對定位與 visibility: hidden 結合,即可避免版面配置位移。
  • polyfill 載入後,這個 @supports 條件就會停止通過,您的內容就會顯示。
  • 在內建容器查詢支援功能的瀏覽器中,條件永遠不會通過,因此頁面會如預期在第一次繪製時顯示。

結論

如果您想在舊版瀏覽器上使用容器查詢,不妨試試polyfill。如果遇到任何問題,歡迎回報問題

我們迫不及待想看到您運用這項服務打造的驚人作品。