來用 CSS function 做半套的 Container Query

fin
7 min readOct 8, 2021

CSS 的 media query 幫助我們依據螢幕 / 視窗尺寸來切換不同的排版方式,是 RWD 的重要基礎。但有的時候我們會需要更細緻的控制,參考某個 ancestor 元素的尺寸來做排版,這就是 Container Query。

隨著瀏覽器的進步,我們已經離 Container Query 越來越接近了。但如果現在、馬上就想要用呢?可以考慮使用一些現成的 lib 如 css-element-queries ,但這些 lib 還是需要額外引入 js polyfill。這篇要介紹的就是使用原生的 CSS function 來達到如同 container query 隨著 container 寬度來做內容尺寸變化的效果。

DEMO

直接看範例最容易了解 https://codepen.io/finfin/pen/QWgXyGN

範例內有許多 CARD,他們都是同樣的 <div class="card></div>,但可以注意到隨著螢幕大小的變化,右側的 CARD 們會改變排版方式,而左側的則不動如山。

精確一點的說,螢幕寬度變化時,左側 sidebar (紫色底)的寬度沒有變化,僅有右側 content (白色底)部分有變化。改變樣式的僅有右側的 CARD,也就是說 CARD 們是依據所在的 container 的寬度,變更自己的寬度。

這個範例完全沒有使用到 JS,接下來就介紹一下如何做到的。

前置知識

你會需要了解 css variable 以及這些 css function 的作用: calc / min /max / clamp

步驟拆解

先來透過 css variable 做一些基本的設定

首先定義變數 base_size,這是用來在後面做計算的參考使用,後面會有這個變數來取 container 寬度,因此一般情況下統一定義為 100% 即可,如有自訂需求也可以在各自的 container 自行定義。

再來,設定我們想要做變化的斷點 breakpoint_wide,範例使用靜態的 600px ,也可以是其他單位甚至是動態計算的結果。

最後 very_big_int 先設定 9999,後面會再介紹。

再來就是 card class , length_for_wide / length_for_not_wide 各自設定不同尺寸時的 card 寬度。這裡裡我們希望超過 600px 時卡片會三個並排,低於 600 時垂直排列,因此寬度各設定為 33.3333% 以及 100%。

--is_wide: clamp(0px, var(--base_size) — var(--breakpoint_wide), 1px)

--is_not_wide: clamp(0px, var(--breakpoint_wide) — var(--base_size), 1px)

第一行行產生會產生這樣的結果:如果 container 寬度大於 601px 則結果為 1px,如果小於 600px 則為 0px,若介於 600 ~ 601 中間則為寬度減 600。再簡化一點來說,小於 600px 時結果為 0px,其他時候大於 0px。

第二行則是反過來算,這裡其實有另一種方式

--is_not_wide: calc(1px — var(--is_wide)

但這方式在寬度落在 600 ~ 601 時會出錯,故不使用,如果確定數值不會有小數點的時候可以考慮此方式。

--dyn_length: calc(min(var(--is_wide) * var(--very_big_int), var(--length_for_wide)) + min(var(--is_not_wide) * var(--very_big_int), var(--length_for_not_wide));

最精華的計算公式,我們先拆解成兩個雷同的部分

min(var(--is_wide) * var(--very_big_int), var(--length_for_wide)

min(var(--is_not_wide) * var(--very_big_int), var(--length_for_not_wide)

var(--is_wide) * var(--very_big_int) 在一般情況下不是 0px * 9999 = 0px 就是 1px * 9999 = 9999px,這個用來與 var(--length_for_wide) 做 min() 的比較之後會變成:is_wide = 1 時採用 length_for_wide 的值,而 is_wide = 0 時則會是 0。

同樣道理第二條算式 is_not_wide = 1 時會採用 length_for_not_wide 的值,is_not_wide = 0 時為 0

我們最後在外面包一個 calc 把兩條算式加在一起,因為 is_wide 以及 is_not_wide 兩者互斥的關係(同一時間只會有一個為 1px),故 dyn_length 就會依據 is_wide / is_not_wide 而分別計算出 length_for_wide 或是 length_for_not_wide,也就是一開始想達成的:

希望超過 600px 時卡片會三個並排,低於 600 時垂直排列,因此寬度各設定為 33.3333% 以及 100%。

最後,把計算的結果指定回 width 就大功告成了

width: var(--dyn_length)

一樣的動圖再貼一次

心得

從整個過程可以看到,我們大量仰賴 calc / clamp / min 來做數值上的操作及模擬 if / else 的切換,但也因為這樣,只能對『數值』做操作,無法真正達到 Container Query 完整的效果,例如: container 在多少 px 以下的時候 display 屬性為 block ,以上的時候為 flexbox。這個方法能做到的是:

Container Width 多少的時候,某某屬性是多少『數值』

另一個要注意的是,其實看一開始的動圖可以發現,會有很明顯的殘影,因為這樣子的計算以及重新渲染是要耗費大量資源的,故這樣的方法不適合用來處理經常動態變動的元素,而比較適合處理同個元素會用在許多不同 Container 的場合。

小測驗

最後來個小測驗,這個是來自 FB 卡片的 CSS,看看你有沒有辦法理解:

參考資料

--

--