在容器查詢 polyfill 中

Gerald Monaco
Gerald Monaco

容器查詢是新的 CSS 功能,可讓您編寫樣式邏輯指定父項元素的地圖項目 (例如寬度或高度),藉此設定子項的樣式。我們最近發布了 polyfill重大更新,與瀏覽器支援到達網頁一樣。

本文將帶您一窺 Polyfill 的運作方式、克服的挑戰,以及運用 Polyfill 為網站提供良好使用者體驗的最佳做法。

深入解析

變頻

當瀏覽器內的 CSS 剖析器遇到不明的規則 (例如全新的 @container 規則) 時,就會將其捨棄,就像在不存在時一樣。因此,Polyfill 的第一要務是將 @container 查詢轉譯為不會捨棄的項目。

轉碼的第一步是將頂層 @container 規則轉換為 @media 查詢。這樣可以確保內容歸為一組。例如使用 CSSOM API 和查看 CSS 來源時。

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

在導入容器查詢之前,CSS 無法任意啟用或停用規則群組。如要實現這個行為,您還需要轉換容器查詢內的規則。每個 @container 都有專屬 ID (例如 123),用於轉換每個選取器,讓只有元素具備 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 提供了安全簡便的解決方法:您可以在 @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 在元素上設定了包含專屬容器 ID 123cq-XYZ 屬性,那麼系統如何支援對這些元素上未設定屬性的虛擬元素?

虛擬元素一律會與 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 自訂屬性。

屬性

容器查詢也會新增 container-typecontainer-name 等 CSS 屬性。由於 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ădureserveUnsplash 提供。