早在 2019 年底,@Zach Leatherman在寻找容器查询起源(地址:https://www.zachleat.com/web/origin-container-queries/)时,找到的最早有关于容器查询的解决方案是 @Andy Hume的基于 JavaScript 的选择器查询和响应式容器的解决方案(地址:https://github.com/ahume/selector-queries)。
2015 年, @Mat ‘Wilto’ Marquis在响应式图片社区小组引入了 元素,将响应式图片带到了响应式 Web 设计的世界,他在《Container Queries: Once More Unto the Breach》(地址:https://alistapart.com/article/container-queries-once-more-unto-the-breach/)一文中概述了元素查询的挑战和使用案例演示了容器查询的特性。
然后,在2017年,@Ethan Marcotte写了一篇关于容器查询相关的文章(地址:https://ethanmarcotte.com/wrote/on-container-queries/),并提出了这样的看法:
在他最初关注的响应式 Web 设计的文章之后的几年里,Web设计师和开发人员的工作越来越集中在组件上,而不是整个页面,这使得媒体查询不那么理想。
从那时起,虽然有很多人主张使用媒体查询,但容器查询向前推进的速度还是不够理想。@L. David Baron在《Thoughts on an implementable path forward for Container Queries》(地址:https://github.com/dbaron/container-queries-implementability)中简明扼要地解释了容器查询向前推进慢的问题出在哪?
容器查询要求样式取决于组件的大小,但考虑到 CSS 的工作原理,组件中的样式会影响其大小。任意打破这个循环,既会产生奇怪的结果,又会干扰浏览器的工作,还会增加浏览器优化的成本。
除了 @David Baron 之外,2018年6月, @Greg Whitworth在荷兰阿姆斯特丹举办的 CSS Day + UX Special(地址:https://noti.st/events/elQrNX/css-day-ux-special)活动上的主题分享《Over the moon for container queries》(地址:https://noti.st/gregwhitworth/UDul7E/over-the-moon-for-container-queries)中也解释了容器查询在Web平台上推进慢的相关原因。更重要的是,@Greg Whitworth还提供了使用新的 JavaScript API 和 CSS 的新技术来实现容器查询的特性。@David Barrrron 也提出了一个可以避免这种困境的策略(地址:https://github.com/dbaron/container-queries-implementability),更重要的是 @Miriam Suzanne在 @David Baron 的策略基础上提出了 @container
方法(地址:https://github.com/oddbird/css-sandbox/blob/main/src/rwd/query/explainer.md)(地址:https://github.com/oddbird/css-sandbox/blob/main/src/rwd/query/explainer.md)。
@container
方法通过对被查询的元素应用大小和布局的限制来实现。任何具有尺寸和布局限制的元素都可以通过一个新的 @container
规则进行查询,其语法与现有的媒体查询类似。
这个提议已经被 W3C 的 CSS 工作组采纳(地址:https://drafts.csswg.org/css-contain-3/),并已经添加到 CSS Containment Module Level 3(地址:https://www.w3.org/TR/css-contain-3/)模块中。有关于该功能的相关问题和各网格平台推进进度,可以点击这里查阅(地址:https://github.com/w3c/csswg-drafts/projects/18)。
虽然 CSS Containment Module Level 3 还是 FPWD 版本,规范中所描述的语法不是最终版本,直到写这篇文章,其语法规则还在变,因此文章中所展示的语法有可能会变以及相关的示例有一天就无效了:
- 什么是容器查询
CSS 容器查询最大的特点是:
容器查询允许开发者定义任何一个元素为包含上下文,查询容器的后代元素可以根据查询容器的大小或计算样式的变化来改变风格!
换句话说,一个查询容器是通过使用容器类型属性(container-type
或 container
)指定要能的查询类型来建立的。适用于其后代的样式规则可以通过使用@container
条件组规则对其进行查询来设定条件。容器查询为响应式设计提供了一种更加动态的方法。这意味着,如果你将此卡片组件放在侧边栏或放在页面主体内部的网格中,则该组件本身根据容器而不是视口进行响应式的信息展示。
首先,把卡片放到一个容器元素中,比如.card__container
:
<!-- HTML --> <div class="card__container"> <div class="card"> <img src="https://picsum.photos/2568/600?random=1" width="2568" height="600" alt="" class="card__thumbnail" /> <div class="card__badge">Must Try</div> <h3 class="card__title">Best Brownies in Town</h3> <p class="card__describe">High quality ingredients and best in-class chef. Light, tender, and easy to make~</p> <button class="card__button">Order now</button> </div> </div>
也就是说,当卡片组件被放在一个容器中时,代表着它被包含在该容器中,比如上面代码中的.card__container
。这也意味着,我们可以使用 CSS 的 container
来查询.card__container
的宽度,并在@container
对 .card
设置不同的样式规则。从而达到设计师真正的意图:
比如,容器宽度(.card__container
)分别在 >400px
、>550px
和 >700px
时为.card
设置不同样式:
代码可能像下面这样:
/* Default */ .card { // ... } /* CSS Container Queries*/ .card__container { container-type: inline-size; } /* container's width > 400px*/ @container size(width > 400px) { .card { // ... } } /* container's width > 550px*/ @container size(width > 550px) { .card { // ... } } /* container's width > 700px*/ @container size(width > 700px) { .card { // ... } }
Untitled@airenCodePen
(地址:https://codepen.io/airen/pen/ZEvoBYL)
(地址:https://codepen.io/)
拖动卡片右下角的滑块,改变 .card__container
容器大小,你可以看到卡片组件(.card
)UI效果的变化:
@container
规则,其工作方式与使用@media
的媒体查询类似,但相反,@container
查询父容器以获取信息,而不是视口和浏览器的UserAgent
。
- 容器查询的使用
到目前为止,CSS 容器查询的语法规则已经经历了多个版本更新,上面示例中展示是最新的使用方式。下面这几篇文章中可以索引到其每个版本的使用方式的差异:
- 初探CSS容器查询
(地址:https://www.w3cplus.com/css/container-queries.html) - 容器查询给设计带来的变化
(地址:https://www.w3cplus.com/css/container-queries-for-design.html) - 容器查询中的
container
和 @container
(地址:https://www.w3cplus.com/css/container-queries-with-container-and-at-container.html)
接下来,通一个容器查询卡片的示例来向大家展示如何使用 CSS 容器查询。
定义一个包含性上下文
要使用 CSS 容器查询特性,首先要定义一个包含性上下文(Containment Context)。这个有点类似于使用 Flexbox 和 Grid 布局(定义Flexbox 或 Grid 上下文使用的是 display
属性),只不过,定义一个包含性的上下文使用的不是我们熟知的 display
属性,而是一个新的CSS属性,即 container
。在一个元素上显式使用 container
可以告诉浏览器以后要针对这个容器进行查询,以及具体如何查询该特定的容器。比如,上面演示的示例中,我们在 .card__container
元素上(.card
的父容器)显式设置了 container-type
的值为 inline-size
:
.card__container { container-type: inline-size }
上面的代码告诉浏览器,可以基于.card__container
容器的内联轴(Inline Axis)方向尺寸变化进行查询。也就是说,当.card__container
容器宽度大小变化到指定的某个值时,其后代元素的样式就可以进行调整。container-type
是 container
属性中的一个子属性,另外,还可以显式使用 container-name
来命名你的容器,即给一个包含性上下文指定一个具体的名称:
.card__container { container-name: card }
这种方式对于同一个上下文中有多个包含性上下文时非常有意义,可以更明确地知道哪些查询会影响元素。
你可以使用简写属性container
,只不过需要在 container-type
和 container-name
之间添加斜杠分割符/
:
.card__container { container-type: inline-size; container-name: card; } /* 等同于 */ .card__container { container: inline-size / card; }
如果一个容器查询被应用到一个没有定义的包含祖先元素上,查询将无法应用。也就是说,无论是 body
还是 html
元素,都没有默认的回退包含上下文。另外,定义包含上下文名称时不能是 CSS 的关键词,比如 default
、inherit
、initial
等。
注意:container-name
可以省略,如果省略将会使用其初始值none
,但 container-type
不可省略,如果省略的话则表示未显式声明包含性上下文!
定义一个容器查询
现在我们知道使用 container
(或其子属性 container-type
和container-name
)对一个元素显式声明包含上下文(对一个元素应用包含性)。有了这个包含性上下文之后,就可以使用 CSS 的 @
规则@container
来对应用了包含性元素进行查询,即对容器进行查询。@container
规则的使用和 @media
以及 @supports
相似:
@container containerName size(width > 45rem) { /* 应用了包含性上下文后代元素的 CSS */ } @container size(width > 45rem) { /* 应用了包含性上下文后代元素的 CSS */ }
这两种方式都是正确的使用姿势,第一个示例中的 containerName
指的是 container-name
显式声明的包含性上下文的名称。如果在@container
中没有指定查询的容器名称,那么这个查询将是针对离样式变化最近的声明了包含性上下文的元素进行查询。比如:
@container size(width > 30em) { .card { border-radius: 20px; } }
表示这个查询将是针对 .card
元素最近的显式声明了包含性上下文的元素进行查询。
代码中的size()
函数是容器查询中的新语法规则。这也是容器查询语法变化之一,即 对查询类型进行了更明确的规定。因为规范已经提高到不仅可以根据尺寸(size
)属性查询,还可以根据样式(style
)属性进行查询。
正如 Terrible Mia(容器查询规范设计者)在 Twitter 上分享的一样,可以使用 style()
函数对样式进行查询:
@container style(--card: large) { /* CSS Style */ } @container size(width > 30em) and style(--card: large) { /* CSS Style */ }
到写这篇文章的时候,还没有浏览器支持对样式进行查询。另外,示例中用于 @container
的查询条件(width > 30em)
相当于 (min-width: 30em)
。使用数学表达式要比使用 min-width
或max-width
更易于理解,自 Media Queries Level 4(地址:https://www.w3.org/TR/mediaqueries-4/#mq-range-context)开始, 在 @media
规则中,也可以使用我们熟悉的数学表达式,比如>=
、<=
等来替代以往不易于理解的min-
和max-
:
上面示例代码中同时出现 container
和 @container
,但他们并不是指的同一个属性,前者是一个CSS属性,后者是一个CSS代码块。而且两者有本质的区别:
container
是container-type
和container-name
的简写属性,用来显式声明某个元素是一个查询容器,并且定义查询容器的类型(可以由container-type
指定)和查询容器的名称(由container-name
指定)。@container
(带有@
规则),它类似于条件CSS中的@media
或@supports
规则,是一个条件组规则,其条件是一个容器查询,它是大小(size
)和(或)样式(style
)查询的布尔组合。只有当其条件为真(true
),@container
规则块中的样式都会被用户代理运用,否则将被视为无效,被用户代理忽略。
容器查询卡片
我想大家对容器查询的理论和概念有了一个初步的认识。接下来,我们把这些东西放到一起,来具体看看前面展示的容器卡片示例是如何实现的。
自从响应式 Web 设计的出现以及移动终端设备越来越多,在设计中也有移动端优先(Mobile First)还是桌面端优先(Desktop First)的争执:
如果你对这方面讨论感兴趣,可以阅读 Ahmad Shadeed 的 《 The State Of Mobile First and Desktop First 》一文。 (地址:https://ishadeed.com/article/the-state-of-mobile-first-and-desktop-first/)
就我个人而言,到目前为止,在开发跨组件状态的“断点”时,将容器查询与考虑“移动端优先”的设计是最有意义的。也就是说,将最窄的视图作为默认样式,然后通过容器查询处理更大宽度的样式更新。
如上图所示,我们从左往右来实现卡片不同状态断点下的UI效果。先从最窄的卡片开始(最左侧,Default 状态)。构建这个卡片组件,所需要的 HTML 结构如下:
<div class="card__container"> <div class="card"> <img src="https://picsum.photos/2568/600?random=1" 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 Grid 来完成卡片的布局。先从最窄的开始,添加下面CSS代码:
.card { display: grid; gap: 1rem; margin: 5vh auto; border-radius: 0.5rem; box-shadow: 0 0.25rem 0.5rem -0.15rem hsla(0 0% 0% / 55%); background-color: #fff; } .card__thumbnail { max-width: 100%; aspect-ratio: 16 / 9; height: auto; object-fit: cover; border-radius: 0.5rem 0.5rem 0 0; } .card__title { font-weight: 700; font-size: clamp(1.2rem, 1.2rem + 3vw, 1.5rem); padding: 0 20px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .card__describe { color: #666; line-height: 1.4; padding: 0 20px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } .card__button { display: inline-flex; justify-content: center; align-items: center; border: none; border-radius: 10rem; background-color: #feca53; padding: 10px 20px; color: #000; text-decoration: none; box-shadow: 0 3px 8px rgb(0 0 0 / 7%); transition: all 0.2s linear; font-weight: 700; justify-self: end; margin: 0 20px 20px 0; cursor: pointer; } .card__button:hover { background-color: #ff9800; }
正如上面的效果所示,卡片组件可以随着其容器(.card__container
)宽度自动变化,在窄屏下效果看上去还不错,但在宽屏下,效果看上去有点怪怪的。不过不用担心,这仅是最初的效果。我们期望的是通过容器查询的特性,在容器不同断点下改变卡片组件的布局。按照前面所介绍的,我们需要先创建一个包含性上下文,即在 .card__container
上使用 container
显式声明该元素是一个包容性上下文。
.card__container { container: inline-size; }
效果如下:
Untitled @airen CodePen (地址:https://codepen.io/airen/pen/MWrXNGM) (地址:https://codepen.io/airen) (地址:https://codepen.io/)
可以在上面示例中尝试拖动卡片右下角滑块改变卡片容器宽度,你将看到的效果如下:
有了这样一个卡片组件之后,如果将其放在不同的位置,即使是同一页面,同一视窗断点下,也会根据其容器断点自动匹配最为适合的布局(或UI效果)。比如:
Untitled @airen CodePen (地址:https://codepen.io/airen/pen/WNdKgMK) (地址:https://codepen.io/airen) (地址:https://codepen.io/)
尝试调整上面示例中视窗的大小:
- 媒体查询 vs. 容器查询
通过上面的示例的介绍,我想你对容器查询特性已经有了一个较清晰的认识了。从使用角度来看,容器查询和媒体查询是非常的相似,那么有人可能会问,有了容器查询是不是就不再需要媒体查询特性了呢?在回答这个问题之前,我们简单的来看两者的差异。
众所周知,媒体查询查询的浏览器视窗宽度(当然还有其他查询特性),而容器查询查询的是组件其父容器(具有包含性上下文的祖先元素)的宽度(或样式)。下图可能可以清晰的阐述两者的差异:
就我个人认为,两者不是谁替代谁的关系,更应该是两者共存的关系。容器查询特性的出现,我们可以不再局限于视窗断点来调整布局或UI样式,还可以基于容器断点来调整布局或UI。换句话说,媒体查询是一种宏观的布局(Macro Layout),可以用于整体页面布局;而容器查询可以调整组件的每个元素,创建了一种微观的布局(Micro Layout)。
- 容器查询解决的是什么问题?
众所周知,响应式设计的概念的核心是 CSS 媒体查询的出现,它允许开发者根据浏览器视窗的尺寸来设置各种样式规则。也正因此,响应式设计和CSS媒体查询开启了更多的 Web 布局解决方案,以及多年来围绕响应视窗尺寸创建的最佳实践。而且,近些年来,设计系统和组件库也得到了更广泛的普及。对于更多开发者而言,更大的期望是:
一次建成,随地部署!
这也意味着一个单独开发的 Web 组件可以在任何情况下工作,以使建立复杂的界面更加有效和一致。只不过,这些组件会组合在一起,形成一个Web页面或Web应用界面。目前,在只有媒体查询的情况下,往往需要额外的一层来协调跨视窗大小变化的组件的突变。在这些情况下,你可能不得不在更多的断点下使用更多的类名来设置不同的样式规则。甚至更惨的是,即使这样做仍然很多情况之下也无法达到最理想的UI表面。
很多时候,响应式Web设计不是关于浏览器视窗尺寸而是关于容器的尺寸大小,比如:
庆幸的是,CSS容器查询的出现,使我们超越了只考虑浏览器视窗尺寸的范围,并允许任何组件或元素对定义的容器尺寸做出响应。因此,虽然你可能仍然使用响应式来给Web页面布局,但Web页面的任何一个组件都可能通过容器查询来定义自己的样式变化。然后,它可以根据它是在一个窄的还是宽的容器中显示,来调整它的样式。
容器查询使我们不再只考虑浏览器视窗尺寸大小,而是允许任何组件或元素对定义的容器尺寸做出响应!
也就是说,有了CSS容器查询,你就能以一种非常精确和可预测的方式定义一个组件的全部样式。
- 设计时考虑容器查询
虽然响应式设计给Web设计师带来了更多的可有性,但响应式设计还是有很多的局限性。对于Web设计师而言,更期待的是能够根据组件容器尺寸来提供不同的设计风格。依旧拿卡片组件来举例:
也就是说,CSS容器查询特性来了之后,作为一名Web设计师,在设计Web页面(或组件)时,就需要基于容器尺寸考虑如何设计。这样一来,可以向Web开发人员提供组件的细节和变化,Web开发人员也可以基于这些细节进行编码(进行开发)。
不过,这并不意味着容器查询特性之后响应式设计是就失去了意义。在未来,容器查询和响应式设计是共存的,简单地说,Web设计师在设计组件时可能会将组件分为以下几个部分:
- 基于视窗(CSS媒体查询)
- 基于容器(CSS容器查询)
- 通用型(不受影响的组件)
比如: