在容器查詢 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 屬性時,才會套用此 ID。在執行階段,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 會提供安全簡單的解決方法:你可以刻意手動將虛擬的 :not(.container-query-polyfill) 選取器新增到 @container 規則中,以增加規則的明確性:

完成前
@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 末端,而非轉換成 #foo::before:where([cq-XYZ~="123"]) (這樣會無效)。

不過,這並不是一切必要的。容器不得修改其「內部」內的內容 (且容器不能本身是內側),但這其實是當 #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 無法與不明或無效屬性搭配使用,因此這些 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 未謹慎解決這些問題,可能導致網站體驗核心指標降低。

為方便您為訪客提供愉快的使用體驗, Polyfill 設計是優先處理首次輸入延遲時間 (FID)累計版面配置位移 (CLS),但可能犧牲最大內容繪製 (LCP)。簡而言之,polyfill 無法保證系統在第一次繪製前會評估容器查詢。因此,為了提供最佳使用者體驗,您「必須」確保在 polyfill 載入並轉譯 CSS 之前,使用容器查詢會影響任何大小或位置的內容。其中一個方法是使用 @supports 規則:

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

建議你在這類元素中加入單純的 CSS 載入動畫,絕對放置在 (隱藏) 內容上方,讓訪客知道發生了什麼事。這類做法的完整示範請見這裡

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

  • 對新版瀏覽器的使用者來說,單純的 CSS 載入器不但能減輕負擔,同時也能為使用舊版瀏覽器和較慢網路的使用者提供輕微回饋。
  • 藉由結合載入器的絕對定位與 visibility: hidden,可避免版面配置位移。
  • 在 Polyfill 載入後,這個 @supports 條件會停止傳遞,而您的內容將顯示。
  • 在內建容器查詢支援的瀏覽器中,這項條件永遠不會通過,因此網頁會在首次顯示時正常顯示。

結論

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

我們迫不及待想看到並體驗您用它打造的精彩作品。

特別銘謝

主頁橫幅由 Dan Cristian Păduregam 提供的 Unsplash 影片。