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,看看你有沒有辦法理解: