目前如何使用容器查詢

最近 Chris Coyier 撰寫了一篇網誌文章,提出了下列問題:

現在所有瀏覽器引擎都支援容器查詢,為什麼開發人員不再使用這項查詢?

阿克一篇貼文列出幾種可能的原因 (例如缺乏意識、老式習慣死亡),但有一個特別的原因。

有些開發人員表示他們現在就想使用容器查詢,但認為不可以因為仍支援舊版瀏覽器

您從標題中得知,我們相信大多數開發人員現在都能在正式環境中使用容器查詢,即便必須支援舊版瀏覽器也沒關係。本文將詳細說明我們推薦的做法。

務實的方法

如想在程式碼中使用容器查詢,但希望在所有瀏覽器中呈現相同的體驗,您可以為不支援容器查詢的瀏覽器導入以 JavaScript 為基礎的備用方案。

隨後,問題會演變:備用的全面性為何?

就像任何備用選項一樣,面臨的挑戰是在實用性與成效之間取得良好平衡。CSS 功能通常無法支援完整的 API (請參閱不使用 polyfill 一文)。不過,您可以先找出大多數開發人員想使用的核心功能組合,然後針對這些功能最佳化備用方案。

但大部分開發人員想執行容器查詢的「核心功能組合」為何?為回答這個問題,建議你思考大多數開發人員如何透過媒體查詢功能,打造回應式網站。

幾乎所有現代設計系統和元件程式庫都已根據行動裝置優先原則標準化,並且使用一組預先定義的中斷點 (例如 SMMDLGXL) 進行實作。根據預設,元件經過最佳化調整,以便在小螢幕上完美顯示,而樣式也會有條件地分層,以支援一組較大的螢幕寬度。(如需相關範例,請參閱 BootstrapTailwind 說明文件)。

這種做法和容器式設計系統相關,因為在大多數情況下,與設計人員相關的內容並不是螢幕或可視區域的大小,而是在放置的環境中,元件可用的空間。換句話說,中斷點會套用至特定內容區域,例如側欄、強制回應對話方塊或後主體,而非根據整個可視區域 (適用於整個網頁) 進行中斷點。

如果您可以基於行動裝置優先的方案 (目前多數開發人員都採用) 的限制,而導入容器型備用方案,那麼導入容器型備用方案,會比導入所有單一容器查詢功能的完整支援功能明顯簡單許多。

下一節將說明這項功能的運作方式,並提供逐步指南,協助您在現有網站上實作這項功能。

運作方式

步驟 1:將元件樣式更新為使用 @container 規則,而非 @media 規則

第一步是找出您認為可透過容器自動調整大小 (而非依據可視區域大小) 調整網站上所有元件的功能。

如要瞭解這項策略的運作方式,建議您一開始先使用一或兩個元件,但如果想將所有元件轉換成容器式樣式,也沒有問題!這項策略的一大好處,就是能在有需要時逐步採用。

找出要更新的元件後,您必須將這些元件 CSS 中的每個 @media 規則變更為 @container 規則。您可以保持相同的大小條件。

如果 CSS 已使用一組預先定義的中斷點,您就可以直接繼續使用這些中斷點。如果您還未使用預先定義的中斷點,就需要為這些中斷點定義名稱 (稍後會在 JavaScript 中參照該點,請參閱步驟 2)。

下方是 .photo-gallery 元件的樣式範例。該元件預設為單一資料欄,然後將樣式更新為 MDXL 中斷點中的兩欄和三個欄 (分別):

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;
}

/* Styles for the `MD` breakpoint */
@media (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Styles for the `XL` breakpoint */
@media (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

如要將這些元件樣式從使用 @media 規則變更為使用 @container 規則,請在程式碼中找出並取代:

/* Before: */
@media (min-width: 768px) { /* ... */ }
@media (min-width: 1280px) { /* ... */ }

/* After: */
@container (min-width: 768px) { /* ... */ }
@container (min-width: 1280px) { /* ... */ }

將元件樣式從 @media 規則更新為以中斷點為基礎的 @container 規則後,下一步就是設定容器元素。

步驟 2:在 HTML 中加入容器元素

上一個步驟依據容器元素大小定義了元件樣式。下一步是定義網頁上哪些元素應該是與 @container 規則相對大小的容器元素。

您可以將任何元素的 container-type 屬性設為 sizeinline-size,藉此將任何元素宣告為 CSS 中的容器元素。如果容器規則以寬度為基準,一般會使用 inline-size

假設網站的基本 HTML 架構如下:

<body>
  <div class="sidebar">...</div>
  <div class="content">...</div>
</body>

如要讓這個網站的 .sidebar.content 元素成為containers,請在 CSS 中加入這項規則:

.content, .sidebar {
  container-type: inline-size;
}

針對支援容器查詢的瀏覽器,只要使用這個 CSS,您就能依據主要內容區域或側欄,將上一步定義的元件樣式設為與主要內容區域或側欄相對應。

不過,如果瀏覽器不支援容器查詢,您還需要進行一些額外的工作。

您需要加入一些程式碼,用於偵測容器元素大小何時變更,並據此更新 DOM,讓 CSS 能夠隨著這些變動更新 DOM。

幸運的是,執行此操作所需的程式碼最小,此程式碼可以完全擷取為共用元件,方便您在任何網站和內容區域中使用。

以下程式碼定義了可重複使用的 <responsive-container> 元素,該元素會自動監聽大小變化,並新增中斷點類別,讓 CSS 可根據該元素設定樣式:

// A mapping of default breakpoint class names and min-width sizes.
// Redefine these as needed based on your site's design.
const defaultBreakpoints = {SM: 512, MD: 768, LG: 1024, XL: 1280};

// A resize observer that monitors size changes to all <responsive-container>
// elements and calls their `updateBreakpoints()` method with the updated size.
const ro = new ResizeObserver((entries) => {
  entries.forEach((e) => e.target.updateBreakpoints(e.contentRect));
});

class ResponsiveContainer extends HTMLElement {
  connectedCallback() {
    const bps = this.getAttribute('breakpoints');
    this.breakpoints = bps ? JSON.parse(bps) : defaultBreakpoints;
    this.name = this.getAttribute('name') || '';
    ro.observe(this);
  }
  disconnectedCallback() {
    ro.unobserve(this);
  }
  updateBreakpoints(contentRect) {
    for (const bp of Object.keys(this.breakpoints)) {
      const minWidth = this.breakpoints[bp];
      const className = this.name ? `${this.name}-${bp}` : bp;
      this.classList.toggle(className, contentRect.width >= minWidth);
    }
  }
}

self.customElements.define('responsive-container', ResponsiveContainer);

這個程式碼的運作方式是建立 ResizeObserver,讓它自動監聽 DOM 中任何 <responsive-container> 元素的大小變更。如果大小變更符合其中一個已定義的中斷點大小,則具有中斷點名稱的類別就會新增至元素 (條件不再符合時則會移除)。

舉例來說,如果 <responsive-container> 元素的 width 介於 768 到 1024 像素之間 (根據在程式碼中設定的預設中斷點值),系統會新增 SMMD 類別,如下所示:

<responsive-container class="SM MD">...</responsive-container>

這些類別可讓您為不支援容器查詢的瀏覽器定義備用樣式 (請參閱步驟 3:在 CSS 中新增備用樣式)。

如要更新先前的 HTML 程式碼來使用這個容器元素,請將側欄和主要內容 <div> 元素改為 <responsive-container> 元素:

<body>
  <responsive-container class="sidebar">...</responsive-container>
  <responsive-container class="content">...</responsive-container>
</body>

在多數情況下,您可以直接使用 <responsive-container> 元素,不用進行任何自訂,但如果需要自訂,可以使用下列選項:

  • 自訂中斷點大小:這個程式碼會使用一組預設的中斷點類別名稱和最小寬度大小,但您可以依個人需求變更這些預設值。您也可以使用 breakpoints 屬性,針對個別元素覆寫這些值。
  • 已命名容器:此程式碼也支援透過傳送 name 屬性的方式支援具名容器。如果您需要為容器元素建立巢狀結構,這點就很重要。詳情請參閱「限制」一節

下列為同時設定這兩種設定選項的範例:

<responsive-container
  name='sidebar'
  breakpoints='{"bp1":500,"bp2":1000,"bp3":1500}'>
</responsive-container>

最後,組合這段程式碼時,請務必使用功能偵測和動態 import(),只在瀏覽器不支援容器查詢時載入該程式碼。

if (!CSS.supports('container-type: inline-size')) {
  import('./path/to/responsive-container.js');
}

步驟 3:在 CSS 中新增備用樣式

這項策略的最後一個步驟是,為無法辨識 @container 規則中定義的樣式的瀏覽器新增備用樣式。如要複製這些規則,請使用針對 <responsive-container> 元素設定的中斷點類別複製這些規則。

延續前述的 .photo-gallery 範例,兩個 @container 規則的備用樣式可能如下所示:

/* Container query styles for the `MD` breakpoint. */
@container (min-width: 768px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Fallback styles for the `MD` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.MD) .photo-gallery {
    grid-template-columns: 1fr 1fr;
  }
}

/* Container query styles for the `XL` breakpoint. */
@container (min-width: 1280px) {
  .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

/* Fallback styles for the `XL` breakpoint. */
@supports not (container-type: inline-size) {
  :where(responsive-container.XL) .photo-gallery {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

在這個程式碼中,如果存在對應的中斷點類別,每項 @container 規則都有等條件符合 <responsive-container> 元素的條件。

<responsive-container> 元素相符的選取器部分已納入在 :where() 函式虛擬類別選取器中,用於保持備用選取器的明確性,與 @container 規則內原始選取器的明確性相同。

每個備用規則也都會納入 @supports 宣告中。雖然備用廣告並非確實必要,但這意味著如果瀏覽器支援容器查詢,就會完全忽略這些規則,這有助於提高樣式比對成效。此外,如果建構工具或 CDN 知道瀏覽器支援容器查詢,且不需要這些備用樣式,或許就能移除這些宣告。

這個備用策略的一大缺點,是需要重複執行樣式宣告兩次,既繁瑣又容易出錯。不過,如果您使用 CSS 預先處理工具,可以將其擷取為可同時產生 @container 規則和備用程式碼的組合。以下是使用 Sass 的範例:

@use 'sass:map';

$breakpoints: (
  'SM': 512px,
  'MD': 576px,
  'LG': 1024px,
  'XL': 1280px,
);

@mixin breakpoint($breakpoint) {
  @container (min-width: #{map.get($breakpoints, $breakpoint)}) {
    @content();
  }
  @supports not (container-type: inline-size) {
    :where(responsive-container.#{$breakpoint}) & {
      @content();
    }
  }
}

取得上述混音後,您可以將原始的 .photo-gallery 元件樣式更新為如下所示,完全消除重複內容:

.photo-gallery {
  display: grid;
  grid-template-columns: 1fr;

  @include breakpoint('MD') {
    grid-template-columns: 1fr 1fr;
  }

  @include breakpoint('XL') {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

這樣就大功告成了!

重點回顧

我們來複習一下,以下說明如何更新程式碼,以在跨瀏覽器備用方案的情況下使用容器查詢。

  1. 識別您要根據容器容器設定樣式的元件,然後將 CSS 中的 @media 規則更新為使用 @container 規則。此外,如果您尚未將一組中斷點名稱標準化,以便符合容器規則的大小條件,請將一組中斷點名稱標準化。
  2. 新增用於自訂 <responsive-container> 元素的 JavaScript,然後將 <responsive-container> 元素加到網頁中要與元件相關的任何內容區域。
  3. 如要支援舊版瀏覽器,請在 CSS 中新增備用樣式,以便與自動新增至 HTML 中 <responsive-container> 元素的中斷點類別相符。最好使用 CSS 預先處理器混合使用,以免重複編寫相同的樣式。

這項策略的好處是只要設定一次,就不用再多費工夫,新增元件及定義容器相關樣式。

實例觀摩

如要瞭解這些步驟可以如何相輔相成,建議您觀看實際操作示範

使用者與容器查詢示範網站互動的影片。使用者調整內容區域大小,展示元件樣式如何根據所含內容區域的大小更新。

本示範是 2019 年建立的網站的更新版本 (在推出容器查詢之前),目的是瞭解為何容器查詢是建構真正回應式元件程式庫的關鍵。

這個網站已針對許多「回應式元件」定義樣式,因此非常適合測試這個不重要的網站所採行的策略。事實證明,更新網站其實非常簡單,而且幾乎不需要更動原本的網站樣式。

您可以前往 GitHub 查看完整示範原始碼,並請務必查看示範元件 CSS,瞭解備用樣式的定義方式。如果您只想測試備用行為,可以選擇只顯示該變化版本的「純備用」示範,即使是支援容器查詢的瀏覽器也一樣。

限制與可能的改善

如本文一開始所述,本文列出的策略適用於大部分開發人員在觸及容器查詢時關心的應用情況。

不過,有些進階用途並未針對這項策略提供支援,以下情況會說明:

容器查詢單元

容器查詢規格定義了幾個新單位,皆與容器大小相關。在某些情況下,我們或許能透過現有方式 (例如百分比) 或使用格線或彈性版面配置來實現大部分的回應式設計。

也就是說,如果您需要使用容器查詢單元,可以利用自訂屬性輕鬆新增相關支援。具體而言,您可以為每個在容器元素中使用的單位定義自訂屬性,如下所示:

responsive-container {
  --cqw: 1cqw;
  --cqh: 1cqh;
}

接著,每當您需要存取容器查詢單元時,請使用這些屬性,而不要使用單元本身:

.photo-gallery {
  font-size: calc(10 * var(--cqw));
}

接著,如要支援舊版瀏覽器,請在 ResizeObserver 回呼的容器元素上,設定這些自訂屬性的值。

class ResponsiveContainer extends HTMLElement {
  // ...
  updateBreakpoints(contentRect) {
    this.style.setProperty('--cqw', `${contentRect.width / 100}px`);
    this.style.setProperty('--cqh', `${contentRect.height / 100}px`);

    // ...
  }
}

如此一來,您就能有效地將這些值從 JavaScript「傳遞」到 CSS,然後您便能完全善用 CSS (例如 calc()min()max()clamp()) 來視需要加以操控。

支援邏輯屬性和寫入模式

您可能已註意到,在某些 CSS 範例的 @container 宣告中使用 inline-size,而不是 width。您可能也注意到,新的 cqicqb 廣告單元分別適用於內嵌和區塊大小。這些新功能反映了 CSS 改用邏輯屬性和值的轉變,而非實體或方向設定。

遺憾的是,Resize Observer 等 API 仍會回報 widthheight 中的值,因此如果您的設計需要邏輯屬性的靈活性,您就必須自行思考。

雖然您可以使用如 getComputedStyle() 傳入容器元素來取得寫入模式,但這麼做會產生費用,但實際上也無法偵測寫入模式是否改變。

因此,最佳做法是讓 <responsive-container> 元素本身接受網站擁有者可視需要設定及更新的書寫模式屬性。如要實作這項功能,請按照上一節的說明操作,然後視需要替換 widthheight

巢狀容器

container-name 屬性可讓您為容器命名,然後再在 @container 規則中參照。如果容器內設有巢狀容器,而且需要只比對特定容器 (而不只是最近的祖系容器) 的特定規則,已命名容器就相當實用。

此處列出的備用策略會使用子系組合,以與特定中斷點類別相符的元素設定樣式。使用巢狀結構容器時可能會中斷,因為多個容器元素祖系中任意數量的中斷點類別可以同時比對特定元件。

舉例來說,這裡有兩個納入 .photo-gallery 元件的 <responsive-container> 元素,但由於外部容器大於內部容器,因此會加入不同的中斷點類別。

<responsive-container class="SM MD LG">
  ...
  <responsive-container class="SM">
    ...
    <div class="photo-gallery">...</div class="photo-gallery">
  </responsive-container>
</responsive-container>

在本範例中,外部容器的 MDLG 類別會影響與 .photo-gallery 元件相符的樣式規則,而樣式規則與容器查詢的行為不符 (因為這些規則只會與最近的祖系容器相符)。

如要處理這種情況,請採取下列任一做法:

  1. 請務必一律為巢狀結構的容器命名,並確保中斷點類別是以該容器名稱開頭,以免發生衝突。
  2. 使用子項組合器,而不要使用備用選取器的子組合 (較限制)。

示範網站的「巢狀容器」部分會舉例說明如何使用已命名的容器,以及程式碼中的 Sass 混合用途,為已命名和未命名的 @container 規則產生備用樣式。

如果瀏覽器不支援 :where()、自訂元素或大小調整觀察器,該怎麼辦?

這些 API 看似相對新穎,但支援的 API 已有超過三年時間在所有瀏覽器中,而且全都是 Baseline 的一部分。

因此,除非您的資料指出「自家網站」的網站訪客大多使用不支援這些功能的瀏覽器,否則不需要備用功能就可以自由使用。

在這個特定用途中,最糟的原因是備用廣告不適用於少數使用者,也就是說,他們看到的會是預設檢視畫面,而不是針對容器大小進行最佳化的資料檢視。

網站的功能應該仍可正常運作,這是真正重要的作用。

為何不只要使用容器查詢 polyfill?

CSS 功能在嚴重的情況下難以執行 polyfill,因此通常需要在 JavaScript 中重新導入瀏覽器的整個 CSS 剖析器和 Cascade 邏輯。因此,CSS polyfill 作者必須做出許多取捨,畢竟通常都會受到許多功能限制和效能負擔的影響。

基於這些原因,我們通常不建議在正式環境中使用 CSS polyfill (包括 Google Chrome 研究室的 container-query-polyfill;由於 Google Chrome 研究室已經停止維護,且主要用於示範用途)。

這裡討論的備用策略限制較少、需要用到的程式碼更少,而且效能明顯優於任何容器查詢 polyfill。

您甚至需要為舊版瀏覽器實作備用機制嗎?

如果您對本文所述的任何限制有疑慮,建議先思考是否需要著手導入備用機制。畢竟,如要避免上述限制,最簡單的方法就是在不提供備用選項的情況下,使用這項功能。事實上,在許多情況下,這個選擇應該是相當合理的選擇。

根據 caniuse.com 的研究,90% 的全球網際網路使用者支援容器查詢;對許多閱讀這篇文章的使用者來說,這個數字可能略高一些。因此,請特別注意,「大部分」使用者都會看到容器查詢版本的使用者介面。對 10% 沒興趣的使用者而言,他們不一定會經歷服務中斷的狀況。採用這項策略時,最糟糕的情況是,某些元件 (而非全球) 會向使用者顯示預設或「行動」的版面配置。

在做出取捨時,最好針對大多數使用者進行最佳化調整,而不是採用最小公分母的方法,為所有使用者提供一致但體驗不彰的體驗。

因此,在假設沒有瀏覽器支援而無法使用容器查詢之前,請確實花一點時間思考採用容器查詢會帶來什麼樣的體驗。即使沒有備用選項,權衡可能也很值得。

展望未來

希望本文內容讓您相信,現在您可以在正式環境中使用容器查詢,而且不必等待好幾年,直到所有不支援的瀏覽器完全消失為止。

雖然這裡列出的策略需要多花點工夫,但必須讓大多數人在網站上採用這個簡單明瞭的架構,儘管如此,仍有許多空間可以更輕鬆地採用。其中一種做法是將許多不同的部分合併為單一元件,並根據特定架構或堆疊進行最佳化,為您處理所有黏土處理工作。如果您打造這類功能,請告訴我們,我們會幫您推廣!

最後,除了容器查詢外,還有許多令人驚豔的 CSS 和 UI 功能,現在還能在各大瀏覽器引擎中互通。身為社群,現在就來瞭解如何使用這些功能,讓我們的使用者受惠。