创意性的CSS布局和灵活Web

发布于 大漠

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特性变化可以说是突飞猛进的。除了FlexboxGrid之外,还有宽高比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特性感兴趣,但又从未接触过,那么请移步阅读下面这些文章:

内在的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属性。

widthheightinline-sizeblock-size 都可以在其前面添加前缀min-max-,用来限制框大小的下限或上限

这些用于决定框大小的CSS属性都可以接受 auto<length-percentage>min-contentmax-contentfit-content(<length-percentage>),除此之外,在未来它们还可以接受 stretchfit-contentcontain(这几个属性值是在 CSS Box Sizing Module Level 4 模块中定义的)。

注意,其中 min-* 开头的属性(min-widthmin-heightmin-inline-sizemin-block-size)的初始值是 auto,它们不接受 none值;反过来,max-*开头的属性(max-widthmax-heightmax-inline-sizemax-block-size)的初始值是 none,它们不接受 auto 值。

这些属性值都是用来决定框尺寸的大小,但它们之间是有差异的,其中有些属性值会让框的大小由框里的内容(元素中的内容)来决定,有些属性值会让框的大小由上下文来决定。换句话说,在CSS中给一个元素框设置大小时,有的是根据元素框内在的内容来决定,有的是根据上下文来决定的。它们分别就是 CSS 中的“内在尺寸(Intrinsic Size)”和“外在尺寸(Extrinsic Size)”:

  • 内在尺寸:元素根据自身的内容(包括其后代元素)决定大小,而不需要考虑其上下文,其中min-contentmax-contentfit-content能根据元素内容来决定元素大小,因此它们统称为内在尺寸
  • 外在尺寸:元素不会考虑自身的内容,而是根据上下文来决定大小,最为典型的案例,就是widthmin-widthmax-width等属性使用了%单位的值

来看一个简单地示例:

上面聊到的是决定盒子大小的,只不过在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的盒模型是计算盒子尺寸;视觉格式化模型是用来计算盒子位置

但对于布局而言,不管是盒子的大小,还是盒子位置,都会影响到布局的呈现!

有关于这方面更详细的介绍,还可以阅读:

花了较长时间聊内在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;
}

正如上面示例所示,使用 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的差异:

最终效果如下:

但对于像下面这种页头,左边有网站的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 中独有的单位,简单地说,1fr(即1fr)就是100%网格容器可用空间;2fr(即2fr)是各50%网格容器可用空间,即1fr50%网格容器可用空间。以此类似,要是你有25fr(即25fr),那么每个fr1fr)就是1/254%。如果你想深入了解fr,可以移步阅读《网格轨道尺寸的设置》一文。

另外,布局中的“导航菜单”和右侧的“用户信息”区域,我们使用的是Flexbox布局:

当然,这两个部分也可以使用Grid来布局。这里不详细阐述。

对于这个示例而言,它是Grid和Flexbox结合的布局案例,也印证了 Ahmad Shadeed 的《Grid for layout, Flexbox for components》文章中提到观点:Grid用于框架级(页面区域)布局,Flexbox用于组件布局。虽然如此,但不能说这种观点就是绝对!

如果你运到像下图这样的一个布局:

毫无疑虑,首先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;
}

你可能已经发现了,虽然有多个网格项目重叠,但这里并没有定位(position)相关属性的身影。在 Grid 布局,我们可以直接使用 grid-columngrid-row属性放置网格项目(指定网格线名称,将网格项目放在指定的位置)上。

我曾花了三篇文章的篇幅(《放置网格项目》、《网格项目的重叠和z轴的层级》和《使用网格构建建重叠布局》)介绍这种重叠布局的技术!

这种布局看上去似乎很复杂,灵活性不够。其实不然,只要我们使用灵活的网格轨道(grid-template-columnsgrid-template-rows定义网格轨道),那么我们的布局仍然会适应内容(比如有更长的标题,段落等)。除了能灵活的适配更多的内容之外,还可以使用grid-columngrid-row移动网格项目,使其在不同的位置呈现。简单地说,可以基于该组件的基础上,得到更多的组件变体。

最后再给大家展示一个使用 CSS Grid 布局的案例:

布局相关的代码:

.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;
}

只不过,今天已经是 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;
}

或者使用 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;
}

值得注意的是,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 取值为 covercontain值时,它具有自己的一套计算规则,它的计算方式类似于 background-size 取值为 covercontain,如果你想进一步了解它是如何计算图片尺寸的,可以移步阅读《图解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%;
}

这可能是我们希望的一种设计(理想的设计)行为。反过来,如果你并希望是这样,你希望让图像保持它的宽高比,而且不受文本列内容长短的影响,我们只需要将Flexbox或Grid容器的 align-items 属性值设置为stretch的其他值。如下所示:

上面看到的示例都是 aspect-ratio 运用于Flex项目或Grid项目上的。其实将它用于Flexbox容器或Grid容器上,也是很棒的。比如:

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

上面这个示例中,每个网格的宽高比都是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;
}

你可以尝试着更少的卡片数量输出时的效果:

即使数据输出更少,你又不希望卡片扩展宽度,只是希望它们居中对齐(或别的对齐方式):

就此效果,选择 Flexbox 也比 Grid 更适合:

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

.item {
    flex: 0 1 19rem;
}

你可以打开浏览器的调试工具,尝试着删除.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;
}

如果输出更少的卡片,效果是符合我们预期的:

不难发现,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 选择器。如果你对该特性感兴趣的话,可以阅读:

虽然添加一些代码能拼凑出所期望的布局效果,但它自身而言响应性是不强的。换句话说,我们并不知道卡片组件被放置的上下文。另外,只要提到响应式,绝大多数的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));
}

其中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()函数。

注意,有关于 CSS 的比较函数方面的内容,我们稍后会和大家一起探讨。这个示例告诉大家,在 CSS Grid 布局中,在某些场景之下,我们是不需要使用媒体查询就能实现响应多的Web布局效果。当然,在一些场景中,Flexbox 布局同样具备这方面的能力,一般是使用 flex-wrap: wrapflex: 1。有关于这方面更详细的介绍,可以阅读 Stephanie Eckles 的 《Container Query Solutions with CSS Grid and Flexbox》一文。你也可以移步阅读下面这几篇文章:

这样的布局看上去已经很完美了,我们不需要任何媒体查询就能实现响应式的效果,但在一些场景中,上面的方式不一定就完美了。依旧拿上面的网格布局为例。网格容器有足够空间的时候,它是一个 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条件组规则对其进行查询来设定条件。

比如上面这个示例,在网格容器.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;
    }
}

拖动卡片组件右下角滑块,改变容器大小,你将看到的效果如下:

容器查询还可以嵌套:

关键代码如下:

<!-- 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);
    }
}

最终的效果如下:

注意,容器查询的名称不一定就显式设置,如果没有指定容器查询名称,那么@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 比较函数仅是 CSS 函数中的一部分,其主要包含 min()max()clamp()三个函数,都可以给这三个函数传入一个列表参数,并根据相应的规则取出一个符合条件的值,除此之外,它们也像 calc() 函数一样做动态计算,即可以做一些数学的四则运算。

min()函数设置最大值,相当于max-width(或max-inline-size),如果用于height(或block-size)时相当于max-heightmax-block-size,取出min()函数中最小的一个值:

max()函数设置一个小值,相当于min-width(或 min-inline-size),如果用于height(或block-size)时相当于min-heightmin-block-size,取出max()函数中最大的一个值:

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))

有关于 比较函数 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:

具体效果如下:

有关于CSS比较函数更多的介绍还可以阅读下面这些文章:

容器查询单位

从《图解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-sizepaddingmargin 组件内的内容时节省我们的精力和时间。我们可以使用容器查询单位代替手动增加字体大小。

也正如 Miriam Suzanne (最初提出提案并且定义规范的作者,容器查询规范定义也是她)所分享的,这些单位在 Chromium 中只要打开容器查询标志就可以在 Chrome Canary 中使用这些单元:

有了这些容器查询单位之后,我们在构建组件时,特别是根据容器查询来调整UI的组件,我们可以像下面这样融入容器查询单位:

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

子网格

不知道大家是否有发现,我们在介绍容器查询时向大家展示的示例,他有不美之处:

比如上图所示,并排的两张卡高度并不一致,直接影响了美感。像上图这样的场景,在实际的 Web 布局中可以说是比比皆是:

我想你也碰到类似上图这种布局。每张卡片的顶部、中间和底部有着不同的内容,但是希望每个部分不管内容多少,他们都是相互对齐的。要实现这种布局效果,使用现有的布局技术总是有一定难度的。换句话说,你使用现有Web布局技术,你实现的效果有可能总是像下图这样:

庆幸的是,CSS Grid Levle2 新增了一个 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;
}

在支持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>样式的差异:

上面这个示例是 :has() 选择器最基础的使用。其实他可以做很多事情,做很多有创意的布局效果,比如 @Jhey 写的一个:has()示例

更多的效果可以查阅《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; 
}

注意,该示例还结合了 数量查询(也称为 范围选择器)选择器!

上面这个示例在现实生产中也是很常见的需求。比如。我有几张使用 CSS Grid 布置的文章预告卡。有些卡片只包含标题和文字,而另一些卡片也有图片。我希望有图像的卡片比没有图像的卡片在网格上占用更多的空间。

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

如果你对 :has() 选择器感兴趣的话,还可以移步阅读下面这几篇文章:

小结

上面和大家聊了一些有关于布局方面的CSS新特性,其实到今天为止,CSS 新增了很多强大、很酷的 CSS 特性,这些特性都有助于我们创建一个有创意的 Web 布局,而且比以往任何时候还要容易的多。我希望大家很我一样,对这个CSS布局的新时代感到兴奋,并且可以看到创意性的可能性,

换句话说,时至今日可用于布局的特性很多,那么对于Web开发者而言,可选择的机会也就越多,同时带来的困惑也就更多。在实际开发时,我们应该根据实际的情景和需求,去选择更适合的布局方案。正如文章中多个示例所示,在现代Web布局中,就技术方面而言将会是你中有我,我中有你。就比如 Flexbox、Grid、容器查询、媒体查询、比较函数和父选择器等等,都组合在一起。这样做,只是更好的构建更具弹性,灵活性,适配性的Web布局。

最后,希望这篇文章给大家能带来一些收获!