創意性的CSS布局和靈活Web

前端晚間課 發佈 2022-09-21T09:53:14.241535+00:00

2022年CSS Day於2022年6月9日和10日在阿姆斯特丹舉行,這是時隔三年後再次舉辦的一次有關於CSS主題的盛會(上一屆是2019年)。

2022年CSS Day於2022年6月9日和10日在阿姆斯特丹舉行,這是時隔三年後再次舉辦的一次有關於CSS主題的盛會(上一屆是2019年)。今年的CSS Day一共有14個關於CSS方面的話題,其中有幾個話題是非常有意思的,比如 Lea Verou 的 《CSS Variable Secrets》、Bramus Van Damme的《The CSS Cascade, a deep dive》、Adam Argyle的《Oh Snap!》和 Michelle Barker 的《Creative CSS Layout》。就我個人而言,Michelle Barker 的 《Creative CSS Layout》話題我最為感興趣,該話題圍繞著 CSS 的一些新技術給Web布局帶來的變化而展開。如果用一句話來描述的話,就是 CSS的新特性可以構建具有創造性的、靈活的Web布局。在接下來的內容中,我們一起來看看這個主題中所闡述的觀點和內容,如果你感興趣的話,請繼續往下閱讀。

文章很長,內容很多,乾貨很多,所以點點關注不迷路,先收藏再說!!

簡介

在過去的幾年時間,有關於Web布局的CSS特性變化可以說是突飛猛進的。除了Flexbox和Grid之外,還有寬高比(aspect-ratio)、CSS比較函數(min()max()clamp())、CSS自定義屬性和CSS邏輯屬性,所有這些都可以幫助我們解決常見的布局挑戰。另外還有一系列的CSS新特性即將出現(有些已經出現在瀏覽器中了),比如子網格(subgrid)、容器查詢(container@container)和父選擇器:has()。作為開發人員,挑戰不再是這些CSS特性怎麼使用,能做些什麼,而是在這麼多CSS新特性中,我們應該如何做出最佳(最為適合)選擇,來構建構建具有創造性、靈活性的Web布局。

上面提到的這些CSS特性,可能對於很多開發者而言是陌生的,但對於我個人而言,這些已不是新特性,在小站上已經有很多關於這方面的詳細介紹,但 Michelle Barker 的分享和PPT還是給我帶來不少的啟示。我在這裡做一下搬運工,並且在搬運的過程中添加一些自己的看法。如果你覺得我太囉嗦,你可以移步閱讀 Michelle Barker 分享的 **PPT**(也可以點擊這裡下載PPT) 和 **視頻**(視頻請自帶天梯)。

該主題大致從以下這幾個方面展開。

2022年及以後的CSS布局技術

自從第一張Web頁面誕生至今,Web的布局已經經過了多次疊代:無布局 » 表格布局 » 浮動布局 » 框架布局 » 現代布局 » 未來布局:

整個布局演變過程中,有不同的名詞來定義布局(一般根據採用的布局技術來命名):

其中浮動布局(主要是 CSS 的 float 屬性)技術曾也占據較長時間,在當初那個年代可以說是主流的布局技術,直到 Flexbox 的出現以及瀏覽器對 Flexbox 越來越完善時,浮動布局技術才被Flexbox布局技術替代下來。雖然時下 Flexbox 布局技術是一個主流布局技術,但並不代表著CSS的浮動(float)就沒有存在的必要了(有些布局效果還是離不開浮動的,比如不規則布局)。

隨著CSS技術不斷向前發展,尤其是這幾年,可以用Web布局的特性明顯地增多:

正如上圖所示,其中多列布局(Multi-column)和 Flexbox 已經是很成熟的技術,只不過多列布局(Multi-column)使用的較少,對於像 CSS 自定義屬性、CSS 網格(它也很早就有了)、寬高比(aspect-ratio)、CSS 比較函數(min()max()clamp())、CSS 邏輯屬性、CSS書寫模式 和 CSS 視窗單位是近兩年才得到主流瀏覽器支持,其他很多特性對於Web開發者來說「只聞其名,未見其身」。

換句話來說,時至今日,這些特性都可以用於 Web 布局當中,它們都是 Web 布局工具箱中的一員。在未來,我們還可以使用像子網格(subgrid)、容器查詢 和 父選擇器 :has() 等特性(這三個特性,已經得到部分主流瀏覽器的支持)。如果你有關注過CSS相關的發展報告的話,你可能也知道,這幾個CSS特性一直以來也是 CSSer 最為期待的三個特性,尤其是容器查詢和父選擇器:

也就是說,這些CSS特性已成為時下或將成為 Web布局的主流技術。

如果你對這裡提到的CSS特性感興趣,但又從未接觸過,那麼請移步閱讀下面這些文章:

  • 圖解CSS:Flexbox布局:Part1、Part2
  • Flexbox布局中不為人知的細節
  • 2022年不能再錯過 CSS 網格布局了
  • 圖解CSS:CSS自定義屬性,更多關於CSS自定義屬性點擊這裡
  • 圖解CSS:CSS邏輯屬性
  • CSS的邏輯屬性對盒模型帶來的變化
  • Web中向左向右
  • 使用CSS的aspect-ratio實現寬高比縮放,有關於寬高比更多介紹請猛擊這裡
  • 聊聊min(),max()和clamp()函數
  • CSS 比較函數構建響應式UI
  • CSS子網格subgrid和嵌套網格
  • 容器查詢中的 container 和 @container,有關於容器查詢更多的介紹可以點擊這裡
  • CSS 的父選擇器:has()

內在的Web設計

內在的Web設計這個概念在 2018 年的時候由 Jen Simmons 提出的,這個概念是 Web 設計中的一個新概念!她在 2018 年的 An Event Apart 大會上分享了該話題(該話題的PPT請點擊這裡獲取),她分享時曾表示:

我們現在正處於Web設計發展的另一個轉折點,創意比增長更重要。

Jen說,「內在Web設計」("Intrinsic Web Design")可能是Web設計歷史上的第六個關鍵點,一切都在改變,在以技術和經驗為基礎,希望以最少的代碼量來實現複雜的Web設計,或者說,Web開發者希望在用最少的代碼和複雜Web設計之間取得完美的平衡。她意識到,以「內在Web設計」將可以把這種平衡趨向於完美。那麼什麼是內在Web設計?

什麼是內在Web設計

自從 Web 誕生以來,Web 開發者一直在使用大量的技巧來完成所有與布局有關的事情。無論是使用浮動(float)還是引用外部第三方 CSS 框架(CSS Frameworks)和庫(比如Bootstrap)將內容放置在Web頁面上想要的任何位置(即布局),幾乎都有一些Hack的身影存在!

浮動的初衷是用於排版的,只不過在那個年代,Web開發者利用其特性來構建Web的布局,而且運用於Web布局很多年。其中大多數第三方的CSS框架和庫都是採用浮動來完成Web的布局!

然而,像 Flexbox 和 Grid 這樣的 CSS 模塊的出現使我們能夠正確的構建我們想要的Web布局(設計),而且沒有任何Hack代碼、第三方CSS框架或JavaScript腳本(指完成Web布局方面)。從本質上講:

能夠以最少的Hack和技巧構建任何你想要的Web布局(或設計) !

也就是說,與將設計人員和開發人員都限制在 Web 的「預定義規則」中不同,內在Web設計(Intrinsic Web Design)使他們能夠靈活地將傳統的、久經考驗的 Web布局技術和現代布局方法和工具(比如 Flexbox,Grid等)結合起來,以便根據Web的內在內容創造獨特的布局

鼓勵設計人員和開發人員將內容放在首位,並允許他們利用所有可用的布局技術和方法以最佳方式在Web頁面上顯示內容,同時保持代碼乾淨和更高效。用最簡單的術語來說,

內在Web設計(Intrinsic Web Design)不是內容以設計為導向(Content Design-Driven) ,而是只專注於讓設計受內容驅動(Design Content-Driven) 。

通俗地說,直到現在,大多數的Web設計和布局都是以設計為導向,因為在構建Web布局時,都是基於設計師提供的設計稿(模板)來完成。因此,你不難發現,現存於線上的很多Web頁面上的元素大小(尺寸)基本上都設置了固定的尺寸,而且這些尺寸是根據最初設計師提供的稿子定義的。事實上呢?Web的數據是動態的,服務端吐出的數據與最初設計稿內容有可能並不匹配(有多,也有少),此時呈現給用戶的Web頁面並不是最佳的(有可能很多空白空間未利用,有可能內容溢出容器,打破布局)。反之,Web的內在尺寸設計就不同,在Web布局時,頁面元素大小是根據真實內容(服務端吐出的數據)來決定的。

內在Web設計的關鍵原則

這麼多年來,流動布局(Fluid Layout)和固定寬度布局(Fixed Width Layout)還是絕大多數開發者採用的布局方式,即使是響應式Web設計(RWD:Responsive Web Design)出現之後,也未得到較大的改善。而Jen提出的內在Web設計(IWD:Intrinsic Web Design)相對而言卻有很大的不同,就拿和 RWD 的三個關鍵原則相比(了解RWD的同學都知道,它具有三大關鍵原則,即流體網格Fluid Grids靈活的圖片Flexible Images媒體查詢Media Queries):

  • RWD具有靈活的圖片(Flexible Images),而根據情況,IWD允許你使用靈活的圖片和固定尺寸的圖片
  • RWD的流體網格(Fluid Grids)僅僅只是列(即流體列 Fluid Columns),它是一維的(這裡所說的Grids並不是CSS的Grid模塊,是我們常說的網格系統),而IWD使用的是二維的流體網格,它的列和行都是流體的,即真自的CSS Grid模塊
  • RWD需要藉助CSS媒體查詢模塊特性才能Web頁面具有響應,而IWD不一定要依賴媒體查詢

注意,這裡所說的響應式設計還是基於2010年著名設計師 Ethan Marcotte 提出的響應式概念。新的響應式概念(或理論)有著較大的變化,感興趣的同學可以閱讀《下一代響應式Web設計:組件驅動式Web設計》一文。

換句話說,每一種設計都有自己的關鍵原則,內在Web設計也是如此。Jen在她的分享中說「內在Web設計具有六個關鍵原則」:

即:

  • Fluid & fixed:內在Web設計不只是使用靈活的圖像,而是提倡根據上下文同時使用固定和流體的方法。此外,利用CSS object-fit 屬性,您現在可以在垂直和水平方向調整圖像大小,而不會讓圖像失去寬高比。
  • Stages of Squishiness:內在Web設計有更寬鬆的選擇,在 CSS Grid 模塊中引入了布局如何響應Web頁面的內在上下文的新方法。對於Web開發者而言,有更寬鬆的選擇,比如網格軌道的尺寸設置,開發者可以給網格軌道設置固定的尺寸大小、根據可用空間讓客戶端自動給網格軌道分配大小(CSS Grid 的fr單位,有點類似Flexbox的flex)、使用minmax(min,max)給網格軌道尺寸大小設置一個範圍或給網格軌道設置auto值,讓其根據其上下文內容來決定網格軌道尺寸。你可以將它們組合起來使用,讓Web元素更好的交互和相互協作。
  • Rows & Columns:指的是在 CSS Grid 模塊的幫助下,內在Web設計使你能夠構建一個真正的二維布局。現在不僅有靈活的列,還有靈活的行以及上一條提到的幾個寬鬆點都可以用於列和行。你甚至可以在塊方向(Block Axis)和內聯軸方向(Inline Axis)創建有意的空白區域。
  • Nested Contexts:內在Web布局中,你可以擁有嵌套的上下文,比如Flexbox(FFC)中嵌套Grid(GFC)、Grid(GFC)嵌套Flexbox(FFC)等,你可以選擇和混合最好的布局方法讓你構建一個最具靈活性的Web布局
  • Ways Expand & Contract:在內在Web設計中引入了幾種新的方法來對Web頁面上的內容進行擴展和收縮,即擠壓和收縮(如靈活的圖片Flexible Images)換行和回流(像處理文本一樣,可以自動換行)添加和刪除空白(比如間距會擴展和收縮)重疊(像定位一樣,一個元素重疊在另一個元素上) 。現在,你可以做很多事情,比如不依賴媒體查詢來根據屏幕大小調整頁面元素的尺寸
  • Media Queries, As Needed:內在Web設計中,是可以不依賴CSS媒體查詢讓Web頁面作出響應的,比如CSS Grid布局中的repeat()minmax()auto-fillauto-fit的組合,比如CSS的比較函數min()max()clamp()都可以讓我們在不依賴CSS媒體查詢實現響應式布局

這就是內在Web設計的關鍵原則,也可以說是內在Web設計的美麗和力量!

值得一提的是,時至今日,原生的 CSS Grid 相關特性也可以運用於 RWD 中,只不過當初提出響應式設計概念時,原生CSS Grid 模塊還不夠完善,瀏覽器對其支持度也欠佳。但這幾年中,CSS 技術得到突飛猛進的發展,而且主流瀏覽器對CSS新持性支持的響應速度越來越快。或者說,我們在不同的時間節點,對Web布局技術提法(或者說概念)是有一定差異的,新的概念對應的新的布局方法。

CSS中的內在尺寸和外在尺寸

對於 Web 布局而言,他有兩個關鍵,即 大小上下文。其中大小是用來確定元素的尺寸,上下文是用來確定視覺呈現的模式。這兩個概念在 CSS 中是最基礎不過的兩個。

在 CSS 的世界中,任何一個元素都會被視作為一個盒子:

每一個盒子就是一個框,框的大小是由 CSS 的盒模型相關屬性決定的。隨著 CSS 邏輯屬性的出現,CSS 的盒模型也可以分為 物理盒模型邏輯盒模型,兩種盒模型都有其對應的 CSS 屬性:

上圖中左側是物理盒模型(老的盒模型),右側是邏輯盒模型(新的盒模型)。

有關於 CSS 盒模型更詳細的介紹可以閱讀《圖解CSS: CSS 盒模型》和《CSS的邏輯屬性對盒模型帶來的變化》。

拋開盒模型中其他屬性不說(比如borderpadding,它們也會影響框的大小),其中 widthheight(物理屬性);inline-sizeblock-size(邏輯屬性)是用來設置框大小最直接的CSS屬性。

width、height、inline-size 和 block-size 都可以在其前面添加前綴min-或max-,用來限制框大小的下限或上限

這些用於決定框大小的CSS屬性都可以接受 auto<length-percentage>min-contentmax-contentfit-content(<length-percentage>),除此之外,在未來它們還可以接受 stretchfit-contentcontain(這幾個屬性值是在 CSS Box Sizing Module Level 4 模塊中定義的)。

注意,其中 min-* 開頭的屬性(min-width、min-height、min-inline-size 和 min-block-size)的初始值是 auto,它們不接受 none值;反過來,max-*開頭的屬性(max-width、max-height、max-inline-size和max-block-size)的初始值是 none,它們不接受 auto 值。

這些屬性值都是用來決定框尺寸的大小,但它們之間是有差異的,其中有些屬性值會讓框的大小由框裡的內容(元素中的內容)來決定,有些屬性值會讓框的大小由上下文來決定。換句話說,在CSS中給一個元素框設置大小時,有的是根據元素框內在的內容來決定,有的是根據上下文來決定的。它們分別就是 CSS 中的「內在尺寸(Intrinsic Size) 」和「外在尺寸(Extrinsic Size) 」:

  • 內在尺寸:元素根據自身的內容(包括其後代元素)決定大小,而不需要考慮其上下文,其中min-contentmax-contentfit-content能根據元素內容來決定元素大小,因此它們統稱為內在尺寸
  • 外在尺寸:元素不會考慮自身的內容,而是根據上下文來決定大小,最為典型的案例,就是widthmin-widthmax-width等屬性使用了%單位的值

來看一個簡單地示例:

內在尺寸 vs 外在尺寸

See the Pen: Learn CSS - Extrinsic sizing vs intrinsic sizing.

上面聊到的是決定盒子大小的,只不過在CSS中,每個盒子可以具有不同類型的盒子。不同類型的盒子又被稱為**視覺格式化模型,也常稱上下文格式化模型**,它主要由 CSS 的 display 屬性來決定。換句話說,display 取不同值時,可以得到不同類型的視覺格式模型,比如大家常說的,BFC、FFC、GFC等。

視覺格式化模型對於Web布局有著決定性的影響,因為它會決定CSS中每個盒子的位置,甚至也會影響到盒子的尺寸大小。尤其是在 Flexbox 和 Grid 布局中,很多時候即使你顯式的設置了一個固定尺寸,也會受到影響。比如在 Flexbox 或 Grid 布局中改變對齊方式,比如Flexbox布局中顯式給Flex項目設置flex:1,比如網格軌道以fr單位來設置等等。這主要是因為,在FFC或GFC模型下,將會影響上下文中的盒子尺寸的計算。具體怎麼影響,這裡就不詳細展開了,就拿內在尺寸fit-contentmax-contentmin-content為例吧,其中fit-content容器的可用空間有極強的關聯:

再比如auto屬性值,當displayblock時,盒子大小和其父容器有關;當displayinline時,它卻只和元素內容有關。

雖然說每個元素在盒模型和視覺格式化模型中都是盒子,但它們有著不同的含義和作用,簡單地說:

盒子是同一個盒子,但兩個模型做著不同的事情。CSS的盒模型是計算盒子尺寸;視覺格式化模型是用來計算盒子位置!

但對於布局而言,不管是盒子的大小,還是盒子位置,都會影響到布局的呈現!

有關於這方面更詳細的介紹,還可以閱讀:

  • 圖解CSS: CSS 盒模型
  • CSS的邏輯屬性對盒模型帶來的變化
  • Web布局:視覺格式化模型
  • Web布局:display 屬性
  • display:contents
  • 圖解CSS:CSS 的值和單位
  • 圖解CSS: 元素尺寸的設置
  • 內在尺寸在Flexbox布局中的運用
  • 內在尺寸在Grid布局中的運用

花了較長時間聊內在Web設計,主要是通過這些基礎性的內容讓大家能更好的領略到內在Web設計的魅力。正如 Jen 分享當中所說:

內在Web設計是Web布局的新時代,她超越了響應式設計。我們正在使用Web本身作為一種媒介(設計受內容驅動),而不是試圖模擬印刷設計(內容以設計為導向)。內在Web設計更為重要的是,不僅上下文的流暢,適應性高,還能夠在Web布局和當前的CSS功能集上發揮真正的創造力。

Flexbox or Grid?

在當下,可用於 Web 布局的 CSS 特性有很多,而且這個集合越來越強大。自從 Flexbox 的兼容性越來越完善時,他替代了浮動布局,成為主流的布局技術。只不過,近幾年來,CSS Grid 快速得到主流瀏覽器的支持,在圈中不乏有了新地聲音,CSS Grid 布局將替代 Flexbox 布局,而且對於 CSS Flexbox 和 CSS Grid 哪個更好的爭執也越來越多。

事實上,他們之間沒有好與壞之分,只有適合與否之說。在某些情況之下,使用 CSS Grid 來布局會比使用 Flexbox 更好,那是因為,在沒有 CSS Grid 之前使用Flexbox 完成布局不一定非常適合,但並能絕對地說 使用 Grid 布局就比 Flexbox 布局更好。因為,他們都是現代Web布局技術,都可以讓我們用最少的代碼,最高效的方式構建更為複雜的 Web 布局,並且它們具有不同但重疊的用例。正如我們將要看到的某些布局,顯然知道是用 Flexbox 或 Grid 構建會更適合,但可以想像其他布局,我們也可以用一個(只用Flexbox 或 Grid)或兩個(同時用Flexbox 和 Grid)構建,所以你可能有一個組件,其中外層是用 Grid 構建的,裡面的東西是用 Flexbox 構建的,反之亦然!他們沒有對與錯,只要有效即可。

除此之外,Flexbox 和 Grid 兩者之間還有很多特性是重疊的:

Flexbox 和 Grid 最大的區別就是,Flexbox 和我們以往所知道的布局技術一樣,是一維布局,而 Grid 是二維布局,Grid 是目前為止唯一具有二維布局能力的。

通常二維布局我們採用 Grid 來布局,而一維布局採用 Flexbox 布局。當然,Grid 也可以用於一維布局,所以大家在使用 Grid 來布局時不要陷入到這樣思維中,即 構建單維的布局而不能使用 Grid 布局

對於 Flexbox 和 Grid 的對比,社區中還有另外一種說法:Grid 更適合用於頁面(框架)布局,Flexbox 更適合用於組件布局

我記得大約在2020年的時候,Ahmad Shadeed 就曾發表過這種觀點,詳細可以閱讀其博文《Grid for layout, Flexbox for components》。

只這種提法也不是絕對的。對於很多Web組件,使用 Grid 將會是一個更好的選擇。

既然如此,我們在構建Web布局(或組件布局)時怎麼選擇才是最為適合的呢?我想通過幾個示例來聊,希望大家能從示例中找到自己想要的答案。

先來看一個簡單的示例:

這是一個再普通不過的一個按鈕組件了。大家肯定會說,這有啥好思考的呢?不管是 Grid 還是 Flexbox ,這不都是分分鐘的事情嗎?如果僅考慮視覺上的展示,的確是如此。使用Grid 和 Flexbox 都可以:

<!-- HTML -->
<button>
    Sign up
    <svg></svg>
</button>

關鍵CSS代碼:

.button--flex {
    display: inline-flex;
    flex-wrap: wrap;
}

.button--grid {
    display: inline-grid;
    grid-template-columns: 1fr auto;
}

See the Pen: Flexbox or Grid.

正如上面示例所示,使用 Flexbox 和 Grid 實現的按鈕視覺效果都是一樣的。就該按鈕而言,如果該按鈕組件放置在某個容器中,且這個容器空間較小時,希望按鈕能自動換行,而不是溢出容器:

上圖左側是我們期望的效果,右側是我們不想要的效果

在這樣的交互或場景下,使用 Flexbox 來構建組件布局要比 Grid 靈活的多。我們只需要在 Flexbox 容器上顯式設置 flex-wrap的值為wrap即可,但是使用 Grid 來構建組件布局的話,要難得多,你不得不去做一些額外的工作,比如根據媒體查詢或容器查詢來改變grid-template-columns的值或者顯式調整網格項目(svg)的放置位置。

小提示,使用容器查詢會更容易一些。

再來看一個導航的示例:

上圖這樣的導航,在 Web 上隨處可見。希望該導航隨著視窗大小改變時,具備下面這樣的效果:

導航菜單項會隨著視窗變小而斷行,並且始終居中顯示。我猜想,熟悉Flexbox的同學應該立馬想到了構建導航的方案。是的,構建該導航,使用Flexbox要比Grid簡單地多,只需要在Flexbox容器上顯式設置flex-wrap:wrapjustify-content:center就可以:

.nav {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
}

菜單項之間的間距,使用了 CSS 的 gap 替代以往我們熟悉的margin屬性,該屬性最早是在Grid模塊中得到支持,現如今同樣可以運用於Flexbox中。gap可以用來設置行與行或列與列之間的間距,並且和容器四邊邊緣不會有額外的間距。下圖可以闡述它與margin的差異:

最終效果如下:

See the Pen: Creative CSS Layout #1: Button(Flexbox or Grid).

但對於像下面這種頁頭,左邊有網站的Logo、中間是一個導航菜單,右側是用戶頭像,暱稱和一個購物車按鈕,而且導航菜單中水平居中的:

估計很多同學首先會使用Flexbox來布局,在Flexbox容器的主軸方向運用兩端對齊即可:

header {
    justify-content: space-between;
    display: flex;
}

初步看上去似乎沒啥問題,Flexbox容器剩餘的空間自動分配給了相鄰的Flex項目之間:

剩餘空間是按space-between分配給了相鄰Flex項目之間,並且第一個Flex項目緊挨著Flexbox容器左側邊緣(內聯軸起始邊緣),最後一個Flex項目緊挨著Flexbox容器右側邊緣(內聯軸的結束邊緣)。或者說,兩端是對齊了,但這個示例中本該水平居中的導航菜單卻有一點點偏左:

造成這種現象的主要原因是,導航菜單兩邊的Flex項目寬度不相等

這並不是你真正想要的效果(或者說符合設計要求的視覺效果)。也就是說,構建這個頁頭的布局,使用 Flexbox 其實是不太適合的,如果你一定要使用 Flexbox 不是不可以,你需要添加額外的代碼。如果使用 Grid 來布局的話,就會簡單地多:

header {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    gap: 1rem;
}

設置了一個三列網格,並且第二列的列寬是根據導航菜單來決定的(auto),並且把 Grid 容器可用空間(除導航占用之外的容器空間)均分成兩等份(第一列和第三列列寬是1fr),一份給了第一列(Logo所占列),另一份給了第三列(用戶頭像,暱稱和購物車按鈕所在列):

fr 單位是 Grid 中獨有的單位,簡單地說,1個fr(即1fr)就是100%網格容器可用空間;2個fr(即2fr)是各50%網格容器可用空間,即1fr是50%網格容器可用空間。以此類似,要是你有25個fr(即25fr),那麼每個fr(1fr)就是1/25或4%。如果你想深入了解fr,可以移步閱讀《網格軌道尺寸的設置》一文。

另外,布局中的「導航菜單」和右側的「用戶信息」區域,我們使用的是Flexbox布局:

當然,這兩個部分也可以使用Grid來布局。這裡不詳細闡述。

對於這個示例而言,它是Grid和Flexbox結合的布局案例,也印證了 Ahmad Shadeed 的《Grid for layout, Flexbox for components》文章中提到觀點:Grid用於框架級(頁面區域)布局,Flexbox用於組件布局。雖然如此,但不能說這種觀點就是絕對!

See the Pen : Nav menu.

如果你運到像下圖這樣的一個布局:

毫無疑慮,首先Grid布局方案!它是一個二維的布局,而且有多個網格項目重疊。對於這樣的Web布局,使用Grid來布局的話是最簡單的:

.grid {
    grid-template-columns: 1fr repeat(10, minmax(3rem, 1fr)) 1fr;
    grid-template-rows: minmax(3rem, auto) 3rem auto 6rem auto;
}

.grid::after {
    grid-column: 1 / -3;
    grid-row: 3;
}

h2 {
    grid-column: 1 / span 8;
    grid-row: 1 / span 2;
}

.grid__img {
    grid-column: 6 / -1;
    grid-row: 2 / span 3;
}

blockquote {
    grid-column: 3 / span 4;
    grid-row: 4 / span 2;
}

p {
    grid-column: 7 / span 5;
    grid-row: 5 / span 1;
}

See the Pen: Grid layout example.

你可能已經發現了,雖然有多個網格項目重疊,但這裡並沒有定位(position)相關屬性的身影。在 Grid 布局,我們可以直接使用 grid-columngrid-row屬性放置網格項目(指定網格線名稱,將網格項目放在指定的位置)上。

我曾花了三篇文章的篇幅(《放置網格項目》、《網格項目的重疊和z軸的層級》和《使用網格構建建重疊布局》)介紹這種重疊布局的技術!

這種布局看上去似乎很複雜,靈活性不夠。其實不然,只要我們使用靈活的網格軌道(grid-template-columnsgrid-template-rows定義網格軌道),那麼我們的布局仍然會適應內容(比如有更長的標題,段落等)。除了能靈活的適配更多的內容之外,還可以使用grid-columngrid-row移動網格項目,使其在不同的位置呈現。簡單地說,可以基於該組件的基礎上,得到更多的組件變體。

最後再給大家展示一個使用 CSS Grid 布局的案例:

See the Pen : Grid layout example.

布局相關的代碼:

.grid {
    display: grid;
    gap: var(--pad);
    grid-template-columns: 1fr repeat(10, minmax(0, 6rem)) 1fr;
    grid-template-rows: 1fr minmax(3rem, auto)1fr;
}

h2 {
    grid-column: 2 / span 6;
    grid-row: 2;
}

.grid__img {
    grid-column: 7 / -1;
    grid-row: 1 / span 3;
}

p {
    grid-column: 2 / span 4;
    grid-row: 3;
}

.grid:nth-child(even) h2 {
    grid-column: span 6 / -2;
}

.grid:nth-child(even) p {
    grid-column: span 4 / -2;
}

.grid:nth-child(2n) .grid__img {
    grid-column: 1 / span 6;
}

.grid:nth-child(3n) .grid__img {
    grid-column: span 6 / -2;
}

.grid:nth-child(4n) .grid__img {
    grid-column: 2 / span 6;
}

這個布局其實是一種經典布局,他有一個專業的術語,叫 Full-Bleed 布局。用下圖來描述,會更清晰一些:

CSS Grid 實現這種布局,只需要在聲明列網格軌道時,第一列和最後一列定義為1fr即可:

你可以嘗試著,調整瀏覽器的視窗的大小,不難發現,視窗越大,兩邊的空白空間就越大,反之就越小。

如果你對 Full-Bleed 布局 感興趣的話,可以移步閱讀《Grid布局案例之構建 Full-Bleed 布局》一文。

這兩個案例都可以說是 CSS Grid 布局的經典案例,在不使用 Grid 而改用其他布局技術,那麼要實現這兩種布局就會困難的多,即使是使用 Flexbox 也是如此。你無法逃脫元素的定位,尺寸計算。即使你初稿完成了,整個布局的靈活性也不夠,要是你不信,你可以嘗試著用非Grid技術來構建它們。

CSS Grid 是非常強大的一種布局技術,但他也是一門最複雜的布局技術,所涉及的CSS知識也很多,我自己也折騰了小半年時間才對其有所了解。如果你想深入了解 CSS Grid的話,你可以在《2022年不能再錯過 CSS 網格布局了》一文中索引到你想要了解和學習的知識點!更多 Grid 教程可移步這裡閱讀。

最後再次強調一下,Flexbox 和 Grid 都是現代Web布局技術,他們之間不存在誰取替誰,也不存在好與壞。他們是可以共存的,相互混合使用的。我們在構建Web頁面和組件時,應該從實際著手,選擇最佳的技術!

寬高比

在 CSS 中,任何一個元素都是一個矩形,至少目前為止是這樣的。一般都是顯式設置 widthheightinline-sizeblock-size 屬性值來指定其大小,而且每個矩形都有寬高比。寬高比的作用是,我們可以通過寬高比和另一方向尺寸來決定元素尺寸大小:

  • 顯式指定寬度,通過寬高比來控制高度
  • 或,顯式指定高度,通過寬高比控制寬度

在2021年之前,CSS 中實現寬高比的效果,都是採用Hack手段來完成的,即使用 padding-toppadding-bottom 以及偽元素 ::before::after 來模擬寬高比:

.aspect-box {
    position: relative;
}

.aspect-box::before {
    display: block;
    content: '';
    width: 100%;
    padding-bottom: calc(100% / (var(--aspect-ratio, 16 / 9)));
}

.aspect-box > :first-child {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

上面示例使用的是絕對定位,如果使用 Flexbox 布局的話,還可以像下面這樣來構建,代碼量相對會少一些:

.aspect-box {
    display: flex;
}

.aspect-box::after {
    display: block;
    content: "";
    padding-bottom: calc(100% / (var(--aspect-ratio, 16 / 9)));
}

.aspect-box > :first-child {
    flex: 1;
}

See the Pen: Creative CSS Layout #4: Aspect Box with padding-top (or padding-bottom).

只不過,今天已經是 2022 年了,我們只需要一個 aspect-ratio 屬性就可以實現寬高比的效果:

.aspect-box {
    aspect-ratio: var(--aspect-ratio, 16 / 9);
}

aspect-ratio 屬性和Flexbox 或 Grid 布局配合起來特別好用,尤其是在處理圖像時,將其和object-fit結合用於調整圖像大小非常的完美。用於 object-fit: cover 構建一個畫廊的縮略圖:

img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--pad);
}

.item {
    aspect-ratio: 1;
}

See the Pen : Aspect ratio grid (simple).

或者使用 object-fit: contain 構建一個Logo圖標展示:

img {
    display: block;
    width: 70%;
    height: 70%;
    object-fit: contain;
}

.grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--pad);
}

.item {
    aspect-ratio: 1;
    display: grid;
    place-items: center;
}

See the Pen : Aspect ratio logo grid.

值得注意的是,object-fit 需要在我們想要「適應」的元素上顯式設置widthheight。比如:

<!-- HTML -->
<div class="item">
    <img src="" alt="" />
</div>

如果我們希望圖像能填充整個.item框,需要像下面這樣寫CSS:

img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

如上面示例所示,很多時候我們想把東西(<img>)放在一個具有寬高比的盒子中(.item)。其實,如果你願意,也不必在<img>外套一個容器(.item),可以直接將aspect-ratio運用於<img>上,這樣一來,就不必同時在img上顯式設置widthheight

img {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
}

object-fit 取值為 cover 或 contain值時,它具有自己的一套計算規則,它的計算方式類似於 background-size 取值為 cover 或 contain,如果你想進一步了解它是如何計算圖片尺寸的,可以移步閱讀《圖解CSS:CSS背景(Part3)》一文,或者閱讀 @shadeed9 的《A Deep Dive Into object-fit And background-size In CSS》一文。

我們回過頭來繼續聊 aspect-ratio 屬性。你可能會擔心因為瀏覽器不支持它,從而打破Web布局。就這一點而言,你不必過於擔心,即使在不支持的瀏覽器中,也不會因為使用了 aspect-ratio 打破布局。比如上面的兩個示例,在不支持 aspect-ratio 的瀏覽器中,你將看到的效果如下圖所示:

如果有必要的話,你可以採取漸進增強的方式,使用 CSS 的 @supports 規則給不支持aspect-ratio 的瀏覽器做一個降級處理:

.item {
    max-height: 20rem;
}

@supports (aspect-ratio: 1) {
    .item {
        aspect-ratio: 1;
        max-height: none;
    }
}

這裡要提出來的是,前面我們採用的padding-toppadding-bottom 實現的寬高比效果,絕不是漸進增強的方式,它只是實現寬高比的一種Hack手段。

接著來看一個aspect-ratio 類似於最小值而不是固定值的行為。拿下圖的效果為例:

其中一側有文本,另一側是圖像。如果我們在圖像上設置了一個 3:2 的寬高比(aspect-ratio: 3 / 2),文本列會有足夠的高的高度來匹配圖像的高度,但文本列的文本較長時,圖像則會增加其高度來匹配文本列的高度。這是因為,不管是 Flexbox 還是 Grid 布局,align-items 的默認值為 stretch。這意味著,如果我們有一個子網格的內容長於寬高比框(假設我們為這些框設置了明確的寬度而不是高度,這是更常見的情況),那麼網格將增長以匹配最長項目的高度,忽略縱橫比。

.cta {
    display: flex;
    flex-wrap: wrap;
}

.cta img {
    aspect-ratio: 3 / 2;
    object-fit: cover;
    flex: 1 1 300px;
    width: 300px;
}

.cta__text-column {
    flex: 1 0 50%;
}

See the Pen : Aspect ratio CTA.

這可能是我們希望的一種設計(理想的設計)行為。反過來,如果你並希望是這樣,你希望讓圖像保持它的寬高比,而且不受文本列內容長短的影響,我們只需要將Flexbox或Grid容器的 align-items 屬性值設置為stretch的其他值。如下所示:

See the Pen: Aspect ratio CTA.

上面看到的示例都是 aspect-ratio 運用於Flex項目或Grid項目上的。其實將它用於Flexbox容器或Grid容器上,也是很棒的。比如:

.grid {
    aspect-ratio: 3 / 2;
    display: grid;
    grid-template: repeat(2, 1fr) / repeat(3, 1fr);
    gap: var(--pad);
}

See the Pen : Aspect ratio grids.

上面這個示例中,每個網格的寬高比都是3:2

aspect-ratio設置在網格容器(.grid)上,而且網格列軌道(grid-template-columns)和行軌道(grid-template-rows)個數與寬高比是等同的(3:2,即三列兩行),同時每個軌道的尺寸是 1fr(它會均分網格容器的可用空間)。從而間接性的設置了網格項目的寬高比是 1:1(相當於 aspect-ratio:1)。使用這種方式來構建畫廊效果是很不錯的。

有關於 aspect-ratio 更詳細的介紹,請移步閱讀《使用CSS的 aspect-ratio 實現寬高比縮放》一文,關於 CSS 寬高比相關的內容,可以點擊這裡閱讀。

容器查詢

一直以來,**容器查詢** 都是 Web 設計師和開發者最為期待的一個特性。今天,她已經來了,是真的來了!

在開始聊容器查詢之前,我們還是從 「Flexbox or Grid」 聊起。雖然我們前面花了一定的篇幅和大家一起探討了這方面的話題。但選擇從這個話題著手和大家聊容器查詢是有意義的。比如,對於下圖這樣的布局,你可能依舊會糾結:Flexbox 還是 Grid

就上圖而言,可能會有一些同學選擇使用 Flexbox,也可能會有一些同學選擇 Grid,就當它是一半一半吧。當然,一半一半並不代表選擇都是對的。我們應該從實際的需求來做出更為適合的(或者說正確的)選擇。比如:

當數據輸出和設計模板不一致時(如,只有五張卡片,或四張卡片),你期望卡片寬度能自動擴展,將剩餘的空間分到相應的卡片身上,如下圖所示:

\

在這種情況之下,Flexbox 是有著先天性的優勢:

main {
    display: flex;
    flex-wrap: wrap;
    gap: var(--pad);
}

.item {
    flex: 1 1 19rem;
}

See the Pen : Flexbox.

你可以嘗試著更少的卡片數量輸出時的效果:

即使數據輸出更少,你又不希望卡片擴展寬度,只是希望它們居中對齊(或別的對齊方式):

就此效果,選擇 Flexbox 也比 Grid 更適合:

main {
    display: flex;
    flex-wrap: wrap;
    gap: var(--pad);
    justify-content: center;
}

.item {
    flex: 0 1 19rem;
}

See the Pen : Flexbox.

你可以打開瀏覽器的調試工具,嘗試著刪除.item,效果會像下面這樣:

注意,這兩個示例中最為關鍵的是 Flexbox 項目中的 flex 屬性,相對而言,flex 是 Flexbox 中最有意思,而且是最為複雜的一個屬性。如果你想深入了解或掌握它的話,那麼強烈建議您閱讀《Flexbox布局中不為人知的細節》、《聊聊Flexbox布局中的flex的演算法》和《你真的了解CSS的flex-basis嗎?》這幾篇文章。

如果您希望每張卡片能按流方式自動去排列:

這樣的效果,使用 Flexbox 不是不可以,只需要保證Flexbox容器的寬度與Flex項目加間距總和正好相等。效果就能完美,比如:

main {
    display: flex;
    flex-wrap: wrap;
    gap: var(--pad);
    padding: var(--pad);
    width: calc(var(--pad) * 4 + 19rem * 3);
}

.item {
    flex: 0 1 19rem;
}

只不過,這樣的Web布局效果,使用 CSS Grid 會比Flexbox 更適合。我們只需要在網格容器上定義好列軌道的尺寸:

main {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: var(--pad);
}

.item {
    grid-column: span 2;
}

See the Pen : Grid.

如果輸出更少的卡片,效果是符合我們預期的:

不難發現,Flexbox布局往往是由內到外,即Flex項目設定尺寸;而Grid布局往往是由外到內,網格容器設定網格軌道的尺寸。如果從彈性角度(項目的擴展或收縮)來說,Flexbox布局是通過 Flex 項目上的 flex(即 flex-shrinkflex-grow)屬性決定,而 Grid 往往是在網格軌道上通過repeat()minmax() 函數以及其獨有的fr單位來決定。

在網格布局中,同樣也能實現前面 Flexbox 構建的布局效果,只是需要額外添加一些CSS代碼,比如:

.item:last-child:nth-child(3n + 1) {
    grid-column: span 6;
}

.item:last-child:nth-child(3n + 2) {
    grid-column: 4 / span 2;
}
.item:nth-last-child(2):nth-child(3n + 1) {
    grid-column: 2 / span 2;
}

注意,這裡需要運用一些高級的組合選擇器,比如示例中的 :last-child:nth-child(3n + 1):last-child:nth-child(3n + 2) 以及 :nth-last-child(2):nth-child(3n + 1),它們又被稱為 數量查詢(也稱為 範圍選擇器),這種特性也適用於 :nth-of-type():nth-last-of-type 選擇器。如果你對該特性感興趣的話,可以閱讀:

  • CSS選擇器:偽類選擇器
  • Quantity Queries for CSS
  • Using CSS Mod Queries with Range Selectors
  • Constructing CSS Quantity Queries On The Fly
  • Use Quantity Queries to Make Your CSS Quantity-Aware
  • Tidy list using Mod Queries and Range Selectors

雖然添加一些代碼能拼湊出所期望的布局效果,但它自身而言響應性是不強的。換句話說,我們並不知道卡片組件被放置的上下文。另外,只要提到響應式,絕大多數的Web開發者首先會想到的是 CSS 媒體查詢,即 藉助媒體查詢@media來查詢視窗尺寸(斷點),從而調整Web布局(或組件布局) 。這種想法沒有錯,但對於今天而言,某些場景之下要實現響應式的效果,不一定要強依賴 CSS 媒體查詢。就好比這個示例,如果希望卡片組件能按順序流動,且具有一定的伸縮擴展性,我們完全就可以使用 CSS Grid 來實現,而且不需要任何一行媒體查詢的代碼。

在 CSS Grid 中,可以使用 repeat()minmax() 函數,結合關鍵詞auto-fit來構建:

main {
    display: grid;
    gap: var(--pad);
    grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}

See the Pen : Grid.

其中minmax(MIN, MAX)函數中有兩個參數,第一個參數表示最小值,第二個參數表示最大值,上面示例中的minmax(19rem, 1fr),表示列網格軌道最小列寬為19rem,最大列寬為1fr,也就是每列的列寬實際上是在 19rem ~ 1fr 這個範圍內。

repeat()函數中的第一個參數都是具體的數值,其實除了使用具體的數值,還可以使用關鍵詞auto-fillauto-fit

  • auto-fill :如果網格容器在相關軸上具有確定的大小或最大大小,則重複次數是最大可能的正整數,不會導致網格溢出其網格容器。如果定義了,將每個軌道視為其最大軌道尺寸大小函數 ( grid-template-rowsgrid-template-columns 用於定義的每個獨立值。 否則,作為最小軌道尺寸函數,將網格間隙加入計算. 如果重複次數過多,那麼重複值是 1 。否則,如果網格容器在相關軸上具有確定的最小尺寸,重複次數是滿足該最低要求的可能的最小正整數。 否則,指定的軌道列表僅重複一次。
  • auto-fit : 行為與 auto-fill 相同,除了放置網格項目後,所有空的重複軌道都將摺疊。空軌道是指沒有流入網格或跨越網格的網格項目。(如果所有軌道都為空,則可能導致所有軌道被摺疊。)摺疊的軌道被視為具有單個固定軌道大小函數為 0px,兩側的槽都摺疊了。為了找到自動重複的軌道數,用戶代理將軌道大小限制為用戶代理指定的值(例如 1px),以避免被零除。
  • 簡單地說,auto-fit 將擴展網格項目以填補可用空間,而auto-fill不會擴展網格項目。相反,auto-fill將保留可用的空間,而不改變網格項目的寬度。比如下圖,可以看出 auto-fitauto-fill 的差異:

你要是將 repeat() 函數和 minmax(min,max)1frauto-fill(或auto-fit)結合起來,可以很容易幫我們實現像下圖這樣的響應式布局效果:

另外,我們還可以在 minmax() 函數中使用 CSS 的比較函數,比如 min()

main {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
    gap: var(--pad);
}

這樣設置會先比較min(19rem, 100%)函數中的值,當100%的計算小於19rem時,則取100%的值,反之則取19rem。然後把min()返回的值傳給 minmax()函數。

See the Pen: Creative CSS Layout #10: Card (with Grid).

注意,有關於 CSS 的比較函數方面的內容,我們稍後會和大家一起探討。這個示例告訴大家,在 CSS Grid 布局中,在某些場景之下,我們是不需要使用媒體查詢就能實現響應多的Web布局效果。當然,在一些場景中,Flexbox 布局同樣具備這方面的能力,一般是使用 flex-wrap: wrapflex: 1。有關於這方面更詳細的介紹,可以閱讀 Stephanie Eckles 的 《Container Query Solutions with CSS Grid and Flexbox》一文。你也可以移步閱讀下面這幾篇文章:

  • CSS Grid中的 auto-fill 和 auto-fit
  • 圖解CSS: 網格中的函數

這樣的布局看上去已經很完美了,我們不需要任何媒體查詢就能實現響應式的效果,但在一些場景中,上面的方式不一定就完美了。依舊拿上面的網格布局為例。網格容器有足夠空間的時候,它是一個 3 x n的(三列多行)的網格布局,看上去不會有任何問題。試想一下,這個網格布局放置在一個較窄的空間中(比如側邊欄),它又是會什麼樣的結果呢?

大部分同學,應該希望在有足夠空間中(比如主列)是一個 3 x n 的網格布局,在沒有空間中(比如側邊欄),它是一個 1 x n 的網格布局,就像是在移動端一樣,卡片從上往下以塊塊的形式堆疊:

像上圖這樣的布局,僅僅依賴媒體查詢是無法實現的,你可能需要在網格容器上設置不同的類名,然後指定不同的網格布局:

.card--main {
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
}

.card--aside {
    grid-template-columns: auto;
}

熟悉 CSS 媒體查詢的同學都知道,CSS 媒體查詢是查詢視窗大小的(還有其他的查詢功能),所以在上面示例中,同一個視窗斷點下出現兩種布局(3 x n網格布局和1 x n網格布局),僅使用媒體查詢是做不到的。慶幸的是,我們有了 CSS 容器查詢,我們可以基於組件的容器的尺寸進行查詢,從而調整布局。

我們可以根據容器asidemain的尺寸(比如寬度)的變化,來調整網格.grid的布局。簡單地說,就是查詢容器寬度來調整其後代元素的布局。這就是我們一直所期待的容器查詢。

CSS 容器查詢最大的特點是:

容器查詢允許開發者定義任何一個元素為包含上下文,查詢容器的後代元素可以根據查詢容器的大小或計算樣式的變化來改變風格!

換句話說,一個查詢容器是通過使用容器類型屬性(container-typecontainer)指定要能的查詢類型來建立的。適用於其後代的樣式規則可以通過使用@container條件組規則對其進行查詢來設定條件。

See the Pen : 容器查詢.

比如上面這個示例,在網格容器.grid的父元素mainaside上使用 container(它是container-namecontainer-type簡寫屬性)指定容器查詢的名稱(container-name)和容器查詢的類型(container-type):

main,
aside {
    container: layout /  inline-size;

    // 等同於
    container-name: layout;
    container-type: inline-size;
}

它會告訴瀏覽器,元素 mainaside 是一個「包含上下文」(對一個元素應用包含性)。有了這個包含性上下文之後,就可以使用 CSS 的 @ 規則 @container 來對應用了包含性元素進行查詢,即對容器進行查詢。@container 規則的使用和 @media 以及 @supports相似:

/* Default */
.grid {
    display: grid;
    gap: var(--pad);
}

/* 容器 aside, main 寬度(inline-size)大於 40em */
@container layout (inline-size > 40em) {
    .grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* 容器 aside, main 寬度(inline-size)大於 65em */
@container layout (inline-size > 65em) {
    .grid {
        grid-template-columns: repeat(4, 1fr);
    }
}

上面示例代碼中同時出現 container@container,但他們並不是指的同一個屬性,前者是一個CSS屬性,後者是一個CSS代碼塊。而且兩者有本質的區別:

  • containercontainer-namecontainer-type 的簡寫屬性,兩者之間使用 / 分隔,即 container = <container-name> / <container-type>,用來顯式聲明某個元素是一個查詢容器,並且定義查詢容器的類型(可以由container-type指定)和查詢容器的名稱(由container-name指定)。
  • **@container**(帶有@規則),它類似於條件CSS中的@media@supports規則,是一個條件組規則,其條件是一個容器查詢,它是大小(size)和(或)樣式(style)查詢的布爾組合。只有當其條件為真(true),@container規則塊中的樣式都會被用戶代理運用,否則將被視為無效,被用戶代理忽略。

在你領略了 CSS 容器查詢特性的強大好處之時,可能也會問,"CSS容器查詢是用來替代CSS媒體查詢"?這個問題就好比前面的「CSS Grid 布局是不是用來替代 CSS Flexbox 布局」?你可能已經知道答案了,CSS 容器查詢不是用來替代 CSS 媒體查詢的,在實際開發當中,完全可以將它們結合起來:

main,
aside {
    container: layout / inline-size;
    padding: var(--pad);
}

.grid {
    display: grid;
    gap: var(--pad);
    margin: 0 auto;
}

@container layout (inline-size > 40em) {
    .grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@container layout (inline-size > 65em) {
    .grid {
        grid-template-columns: repeat(4, 1fr);
    }
}

@media (min-width: 50em) {
    body {
        display: grid;
        gap: var(--pad);
        grid-template-columns: 2fr 28rem;
    }
}

你將看到的效果如下:

CSS 容器查詢除了可以用於 Web 布局之外,還可以用於組件之上。同樣拿卡片組件為例。

我們希望同一個 card 組件能隨著其容器的寬度不同時調整組件UI的樣式。在沒有容器查詢時,我們一般是通過添加額外的類名或定義不同的組件來構建像上圖這樣的卡片組件的UI。有了容器查詢查詢特性,構建這樣的卡片組件就輕易地多,我們只需要在組件.card上外添加一個容器.card__container,並且使用 container 定義它是一個包含性上下文即可。

來看一個具體的示例:

<!-- HTML -->
<div class="card__container">
    <div class="card">
        <img src="https://picsum.photos/2568/600" width="2568" height="600" alt="" class="card__thumbnail" />
        <h3 class="card__title">Container Queries Rule</h3>
        <p class="card__describe">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quis magni eveniet natus nulla distinctio eaque?</p>
        <button class="card__button">Order now</button>
    </div>
</div>

/* CSS關鍵代碼 */

/* Defining containment */
.card__container {
    container: component / inline-size;
}

/* The card container width is greater than or equal to 400px */

@container component (width >= 400px) {
    .card {
        grid-template-columns: 180px 1fr;
        grid-template-areas:
            "thumbnail title"
            "thumbnail describe"
            "thumbnail button";
        gap: 10px 20px;
        align-items: start;
    }

    .card__thumbnail {
        grid-area: thumbnail;
    }

    .card__title {
        grid-area: title;
    }

    .card__describe {
        grid-area: describe;
    }

    .card__button {
        grid-area: button;
    }
}

/* The card container width is greater than or equal to 550px */
@container component (width >= 550px) {
    .card {
        grid-template-columns: 240px 1fr;
        grid-template-rows: 56px auto 60px;
    }
}

/* The card container width is greater than or equal to 550px */
@container component (width >= 700px) {
    .card {
        grid-template-areas:
        "thumbnail"
        "title"
        "describe";
        grid-template-columns: auto;
        grid-template-rows: auto auto auto;
        gap: 1rem;
    }

    .card::before {
        content: "";
        display: block;
        grid-row: 1 / -1;
        grid-column: 1 / -1;
        z-index: 2;
    }

    .card__thumbnail {
        grid-row: 1 / -1;
    }
}

See the Pen : Container Queries.

拖動卡片組件右下角滑塊,改變容器大小,你將看到的效果如下:

容器查詢還可以嵌套:

關鍵代碼如下:

<!-- HTML -->
<main><!-- 定義一個名為 layout 的容器查詢 -->
    <div class="grid"><!-- 根據main容器寬度,調整網布局 -->
        <div class="card__container"><!-- 定義一個名為 component 的容器查詢 -->
            <Card /><!-- 根據卡片容器 card__container 的寬度調整 Card 組件UI -->
        </div>
    </div>
</main>

<aside><!-- 定義一個名為 layout 的容器查詢 -->
    <div class="grid"><!-- 根據main容器寬度,調整網布局 -->
        <div class="card__container"><!-- 定義一個名為 component 的容器查詢 -->
            <Card /><!-- 根據卡片容器 card__container 的寬度調整 Card 組件UI -->
        </div>
    </div>
</aside>

/* CSS */
main,
aside {
    container: layout / inline-size;
    padding: var(--pad);
}

.grid {
    display: grid;
    gap: var(--pad);
}

@container layout (inline-size > 40em) {
    .grid {
        grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
    }
}

@media (min-width: 50em) {
    body {
        display: grid;
        gap: var(--pad);
        grid-template-columns: 2fr 28rem;
    }
}

@media (min-width: 75em) {
    .grid {
        border: 0.2rem solid aliceblue;
        padding: var(--pad);
    }
}

.card__container {
    container: component / inline-size;
}

.card {
    display: grid;
    gap: var(--pad-sm);
    grid-template-areas: 
        "thumb"
        "title"
        "describe"
        "link"
}

.card img {
    grid-area: thumb;
}

.card__top {
    grid-area: thumb;
    z-index: 2;
    align-self: start;
}

.card h3 {
    grid-area: title;
}

.card p:not(.card__top) {
    grid-area: describe;

}

.card a  {
    grid-area: link;
}

@container layout (inline-size > 40em) {
    main .card__container:first-child:nth-child(1) {
        grid-column: span 2;
    }
}

@container component (inline-size > 37.25em) {
    .card {
        grid-template-columns: 300px 1fr;
    grid-template-areas:
        "thumb title"
        "thumb describe"
        "thumb link";
    gap: var(--pad-xs) var(--pad);
    }
}

See the Pen : Grid with container queries.

最終的效果如下:

注意,容器查詢的名稱不一定就顯式設置,如果沒有指定容器查詢名稱,那麼@container 可以不使用容器查詢的名稱。如果@container 規則中未指定容器查詢的名稱,它會選擇離他自己的定義了容器查詢的容器。比如:

main, aside {
    container: layout / inline-size;
}

.card__container {
    container-type: inline-size;
}

/* 將相對於 .card__container 容器進行查詢 */
@container (inline-size > 30em) {
    .card {
        //....
    }
}

/* 將相對於 main 或 aside 容器進行查詢 */
@container layout (inline-size > 60em) {
    .card {
        //...
    }
}

這個示例也再次說明,容器查詢和媒體查詢兩者不是誰替代誰的關係,更應該是兩者共存的關係。容器查詢特性的出現,我們可以不再局限於視窗斷點來調整布局或UI樣式,還可以基於容器斷點來調整布局或UI。換句話說,媒體查詢是一種宏觀的布局(Macro Layout),可以用於整體頁面布局;而容器查詢可以調整組件的每個元素,創建了一種微觀的布局(Micro Layout)。

有關於容器查詢相關的介紹就介紹到這裡了,如果你對這方面知識感興趣的話,還可以閱讀:

  • 初探CSS容器查詢
  • 容器查詢給設計帶來的變化
  • 容器查詢中的 container 和 @container
  • 下一代響應式Web設計:組件驅動式Web設計

比較函數

CSS 比較函數僅是 CSS 函數中的一部分,其主要包含 min()max()clamp()三個函數,都可以給這三個函數傳入一個列表參數,並根據相應的規則取出一個符合條件的值,除此之外,它們也像 calc() 函數一樣做動態計算,即可以做一些數學的四則運算。

min()函數設置最大值,相當於max-width(或max-inline-size),如果用於height(或block-size)時相當於max-heightmax-block-size,取出min()函數中最小的一個值:

動圖封面

See the Pen :min() Function.

max()函數設置一個小值,相當於min-width(或 min-inline-size),如果用於height(或block-size)時相當於min-heightmin-block-size,取出max()函數中最大的一個值:

動圖封面

See the Pen: max() Function.

clamp()函數min()以及max()不同,它返回的是一個區間值。clamp()函數接受三個參數,即 clamp(MIN, VAL, MAX),其中MIN表示最小值,VAL表示首選值,MAX表示最大值。它們之間:

  • 如果VALMINMAX之間,則使用VAL作為函數的返回值;
  • 如果VAL大於MAX,則使用MAX作為函數的返回值;
  • 如果VAL小於MIN,則使用MIN作為函數的返回值

如果使用了clamp()函數的話,相當於使用了min()max()函數,具體地說:

clamp(MIN, VAL, MAX) = max(MIN, min(VAL, MAX))

動圖封面

See the Pen :clamp() Function.
有關於 比較函數 min()、max() 和 clamp() 更詳細的介紹可以閱讀《 聊聊min(),max()和clamp()函數》一文。

CSS 比較函數同樣適用於 Web 布局中。比如說,前面卡片的間距gap,我們期望他在兩個值中取一個相對較小的一個值,我們可以像下面這樣做:

:root {
    --pad: min(2rem, 2vw);
}

.grid {
    gap: var(--pad);
}

反之,如果希望取兩個值中的一個較大的值作為卡片之間的間距,可以像下面這樣使用:

:root {
    --pad: max(2rem, 2vw);
}

.grid {
    gap: var(--pad);
}

如果你希望給卡片之間的間距設置一個下限值(min),和一個上限值(max),那麼使用 clamp() 是最好的:

:root {
    --pad: clamp(1rem, 2vw, 3rem);
}

.grid {
    gap: var(--pad);
}

注意,比較函數中的 clamp() 函數就像是一把鎖一樣(在CSS中也稱為 **CSS Locks**),以往是通過 calc()來實現類似的功能。有關於 CSS 鎖這裡就不做過多闡述。

在 CSS 中, CSS 比較函數可以結合 CSS 自定義屬性和 calc() 函數一起使用,這樣會讓你的布局更靈活,同樣拿間距為例:

:root {
    --pad: clamp(1rem, 2vw, 3rem);
    --pad-lg: calc(var(--pad) * 2);
    --pad-sm: calc(var(--pad) / 2);
    --pad-xs: calc(var(--pad) / 4);
}

另外,CSS比較函數中的clamp()常用於font-size,在社區中有很多在線的工具,來生成clamp()函數的參數列表,比如 Modern Fluid Typography Tool:

body {
    font-size: clamp(2rem, 3.2vw + 1rem, 3rem);
}

注意,在這裡面有很一定的數學原理,如果你想更深入的了解這方面的原理,可以閱讀 Adrian Bece 的《Modern Fluid Typography Using CSS Clamp》一文。

上面我們看到的只是 CSS 比較函數最基礎的的使用,事實上,它們還可以用於構建具有響應式UI。在此之外,我們在構建移動端的Web頁面時,為了能更好的適配各種不同的終端設備,我們採用的都是較為粗暴的縮放模式,不管是 REM 適配還是 VW適配方案,都是如此。如今,我們可以使用 CSS 比較函數來構建更為精確的且具有響應式的UI來適配不同的終端。比如下面這個Demo:

See the Pen : Card UI viewport scaling.

具體效果如下:

有關於CSS比較函數更多的介紹還可以閱讀下面這些文章:

  • 給CSS加把鎖
  • CSS中的動態計算
  • CSS的calc()函數
  • CSS函數
  • 聊聊min(),max()和clamp()函數
  • CSS 比較函數構建響應式UI
  • 如何構建一個完美縮放的UI界面

容器查詢單位

從《圖解CSS:CSS 的值和單位》一文中我們可以獲知,在 CSS 中有很多不同類型的 CSS 單位,比如我們熟悉的 pxemrem%等,以及後面新增的視窗單位 vwvhvminvmax等。雖然這些 CSS 單位已經能滿足實際開發中很多場景,但 Web 開發人員經常有另外的需求。我們都知道,用戶在滾動頁面時,瀏覽器視窗的尺寸會發生變化,尤其是像移動端上的 Safari 瀏覽器,他會動態隱藏 URL 欄,這個時候會造成視窗高度有變化。Web開發人員期望有一些新的視窗單位能滿足這樣的場景。

為些,在2021年,有了新的視窗單位出現,比如:

  • 100svh指最小可能視窗高度的 100%
  • 100lvh指最大可能視窗高度的 100%
  • 100dvh指的是 100% 動態視窗的高度

這意味著該值將隨著用戶滾動而改變。也被稱為動態視窗單位:

\

除此之外,還有用於寬度的類似視窗單位,比如 svwlvwdvw。為了涵蓋 vminvmax 的小型、大型和動態版本,實現了svminsvmaxlvminlvmaxdvmindvmax單位。為了支持邏輯屬性,在視口內聯和視口塊維度中,新的vivb類似於現有的視口單元。並且svi, svb, lvi, lvb,dvidvb為內聯和塊維度的小、大和動態版本提供邏輯維度視窗單位。

前面我們在探容器查詢的時候已經了解到,媒體查詢查詢視窗大小,容器查詢是查詢容器大小(注意,不管是媒體查詢還是容器查詢,除了查詢大小之外,還可以查詢其他的)。在單位上也是相似的,視窗單位是相對於瀏覽器視窗大小計算,在 CSS 中也新增了 容器查詢單位

\

\

容器查詢單位是相對於查詢容器尺寸的計算。現有的容器查詢單位主要有:

  • 1cqw等於查詢容器寬度的 1%
  • 1cqh等於查詢容器高度的 1%
  • 1cqi等於查詢容器內聯大小的 1%
  • 1cqb等於查詢容器塊大小的 1%
  • 1cqmin等於1cqi1cqb中較小的一個值
  • 1cqmax等於1cqi1cqb中較大的一個值

正如 Ahmad Shadeed 的 《CSS Container Query Units》文中所提到的:

容器查詢單位可以在處理諸如 font-size、 padding 和 margin 組件內的內容時節省我們的精力和時間。我們可以使用容器查詢單位代替手動增加字體大小。

也正如 Miriam Suzanne (最初提出提案並且定義規範的作者,容器查詢規範定義也是她)所分享的,這些單位在 Chromium 中只要打開容器查詢標誌就可以在 Chrome Canary 中使用這些單元:

有了這些容器查詢單位之後,我們在構建組件時,特別是根據容器查詢來調整UI的組件,我們可以像下面這樣融入容器查詢單位:

.card h3 {
    font-size: clamp(1.2rem, 5cqi + 1rem, 3rem);
}

子網格

不知道大家是否有發現,我們在介紹容器查詢時向大家展示的示例,他有不美之處:

比如上圖所示,並排的兩張卡高度並不一致,直接影響了美感。像上圖這樣的場景,在實際的 Web 布局中可以說是比比皆是:

\

\

我想你也碰到類似上圖這種布局。每張卡片的頂部、中間和底部有著不同的內容,但是希望每個部分不管內容多少,他們都是相互對齊的。要實現這種布局效果,使用現有的布局技術總是有一定難度的。換句話說,你使用現有Web布局技術,你實現的效果有可能總是像下圖這樣:

慶幸的是,CSS Grid Levle2 新增了一個 subgrid 特性,使用該特性,我們可以很輕易實現這樣的布局效果:

我們來看一個具體的實例:

See the Pen :subgrid.

有了subgrid之後,我們可以讓每張卡片跨越三行,然後將每張卡片.card聲明為網格容器,並且顯式設置行網格軌道為subgrid

.card {
    grid-row: span 3;
    display: grid;
    gap: 0;
    grid-template-rows: subgrid;
}

這樣一來,網格.card就會繼續父網格body的網格軌道和間距等。就這麼簡單的幾個屬性,卡片中的頂部、中間和底部就神奇的對齊了:

到目前為止,你在 Firefox 和 Safari TP (16.0) 瀏覽器上都可以看到 subgrid 的效果,而且 Chrome 瀏覽器也在迎頭趕上。因此你將在不同的瀏覽器上會看到不同的效果:

對於不支持 subgrid 的瀏覽器,也可以像 aspect-ratio 那樣,使用 @supports 對其做漸進增強的處理:

@supports (grid-template-rows: subgrid) {
    .card {
        grid-template-rows: subgrid
    }
}

上面看到的示例是最常見的一個可用subgrid構建的,事實上,subgrid 可用的地方很多,比如像下面這樣的布局:

在這個卡片組件中(網格),卡片標題和圖片標註需要左側對齊:

\

我們可以在卡片.card上像下面這樣定義一個網格:

.card {
    display: grid;
    grid-template-columns: 1fr 1fr 50px 50px 1fr 1fr;
    grid-template-rows: repeat(3, min-content);
    gap: 1rem 1.25rem;
    align-content: start;
    align-items: start;
}

使用瀏覽器調試工具,可以看到網格像下面這樣:

同時figure跨五列兩行:

figure {
    grid-row: 2 / span 2;
    grid-column: 1 / span 5;
}

此時,在figure上使用subgrid,並將imgfigcaption按網格線放置到指定位置:

figure {
    display: grid;
    grid-template-rows: subgrid;
    grid-template-columns: subgrid;
}

figure img {
    grid-column: 1 / span 3;
    grid-row: 1 / span 2;
}

figcaption {
    grid-row: 2;
    grid-column: 4 / span 2;
}

See the Pen :subgrid

在支持subgrid的瀏覽器中,看到的效果如下:

有關於subgrid更詳細的介紹可以閱讀《網格布局:子網格 vs. 嵌套網格》一文。

父選擇器:has()

父選擇器 :has()像容器查詢一樣,一直以來都是 Web 開發者所期待的一個選擇器。在 CSS 選擇器 Level 4版本新增了該選擇器,只不過到近一兩年才得到部分主流瀏覽器支持。和他一起出現的選擇器還有:is() 和 :where() 選擇器。 不過,我們這裡只和大家一起簡單的聊一下 :has() 選擇器。

用一句話來描述:

:has()選擇器是一個關係型偽類選擇器,也被稱為函數型偽類選擇器,它和 :is()、:not() 以及 :where()函數型選擇器被稱為 CSS的邏輯組合選擇器 !

在我們構建Web布局或Web組件時,:has()可以起很大的作用。比如下面這個示例:

上圖中最大的差異,就是有圖片和沒有圖片的差異。在以往沒有父選擇器的時候,我們需要添加額外的類名來做布局差異性的控制。有了 :has() 選擇器,事情就要簡單的很多。

我們來看一個更簡單地示例。想像一下,我們想<figure>根據圖中的內容類型來設置元素的樣式。有時我們的圖形只包含一個圖像:

<figure>
    <img src="flowers.jpg" alt="spring flowers">
</figure>

而其他時候有一個帶有標題的圖像:

<figure>
    <img src="dog.jpg" alt="black dog smiling in the sun">
    <figcaption>Maggie loves being outside off-leash.</figcaption>
</figure>

現在,讓我們只在包含有 <figcaption><figure> 元素上設置一些樣式:

figure:has(figcaption) {
    background: white;
    padding: 0.6rem;
}

這個時候,你可以看到包含和沒有包含<figcaption><figure>樣式的差異:

See the Pen : :has() Demo #1 — Figure variations.

上面這個示例是 :has() 選擇器最基礎的使用。其實他可以做很多事情,做很多有創意的布局效果,比如 @Jhey 寫的一個:has()示例:

See the Pen : CSS OS Dock [CSS :has(), lerp custom props].
更多的效果可以查閱《CSS 的父選擇器 :has()》文中提到的案例!

你可能也已經想到了,:has() 選擇器還可以和 CSS Grid 結合起來,構建更多網格布局。比如:

.grid { 
    display: grid; 
    grid-template: 1fr / repeat(var(—cols, 3), 1fr); 
    grid-auto-rows: 1fr; 
}

.grid:has(:last-child:nth-child(even)) { 
    —cols: 2; 
}

.grid:has(:last-child:nth-child(5)) { 
    —cols: 2; 
}

.grid:has(:last-child:nth-child(5)) .item:first-child { 
    grid-column: span 2; 
}

See the Pen : Grid + quantity queries + aspect-ratio.
注意,該示例還結合了 數量查詢(也稱為 範圍選擇器)選擇器!

上面這個示例在現實生產中也是很常見的需求。比如。我有幾張使用 CSS Grid 布置的文章預告卡。有些卡片只包含標題和文字,而另一些卡片也有圖片。我希望有圖像的卡片比沒有圖像的卡片在網格上占用更多的空間。

article:has(img) {
    grid-column: span 2;
    grid-row: span 2;
}

See the Pen: :has() Demo #2 — Teaser cards

如果你對 :has() 選擇器感興趣的話,還可以移步閱讀下面這幾篇文章:

  • Using :has() as a CSS Parent Selector and much more
  • CSS 的父選擇器 :has()
  • CSS 選擇器 :is() 和 :where() 與 :has() 有什麼功能
  • 初探CSS 選擇器Level 4

小結

上面和大家聊了一些有關於布局方面的CSS新特性,其實到今天為止,CSS 新增了很多強大、很酷的 CSS 特性,這些特性都有助於我們創建一個有創意的 Web 布局,而且比以往任何時候還要容易的多。我希望大家很我一樣,對這個CSS布局的新時代感到興奮,並且可以看到創意性的可能性,

換句話說,時至今日可用於布局的特性很多,那麼對於Web開發者而言,可選擇的機會也就越多,同時帶來的困惑也就更多。在實際開發時,我們應該根據實際的情景和需求,去選擇更適合的布局方案。正如文章中多個示例所示,在現代Web布局中,就技術方面而言將會是你中有我,我中有你。就比如 Flexbox、Grid、容器查詢、媒體查詢、比較函數和父選擇器等等,都組合在一起。這樣做,只是更好的構建更具彈性,靈活性,適配性的Web布局。

最後,希望這篇文章給大家能帶來一些收穫!

文章來自大漠老師,https://zhuanlan.zhihu.com/p/561874666

關鍵字: