前端开发者学堂 - fedev.cn

容器查询中的 container 和 @container

发布于 大漠

从《初探CSS容器查询》一文中我们知道了容器查询是是什么以及它的作用。使用 contain来创建一个包含上下文,同时使用 @container 规则,可以在符合条件下为查询容器的后代单独指定样式规则。简单地说,可以使用 CSS 容器查询,让同一个组件根据其容器的不同尺寸向用户呈现不同的UI效果。该特性目前是 CSS Containment Level 3 中的一个特性,这个功能模块中除了 contain@container 之外,还新增了一个容器属性 container,也可以使用 container@container 结合实现容器查询效果。在这篇文章中,我们就来看看 container@container 是如何工作的,它们可能会有哪些功能!感兴趣的同学,请继续往下阅读!

容器查询的相关背景

CSS 的 媒体查询 引发了一场响应式设计的革命,为开发者提供了一种方法来查询用户代理或设备环境的各个方面,比如视窗的大小或用户偏好来改变 Web 页面的风格。直到现在,媒体查询还做不到让元素的样式能根据一个最近的容器的大小来改变样式网格。也正因此,大家一直期待的容器查询来了。

如果你对用户偏好设置是如何向用户呈现不同样式风格感兴趣,那么可以阅读《CSS媒体查询新特性》和《系统偏好设置的那些事儿》。

CSS 容器查询最大的特点是:

容器查询允许开发者定义任何一个元素为包含上下文,查询容器的后代元素可以根据查询容器的大小或计算样式的变化来改变风格

换句话说,一个查询容器是通过使用容器类型属性(container-typecontainer)指定要能的查询类型来建立的。适用于其后代的样式规则可以通过使用@container条件组规则对其进行查询来设定条件。

例如,我们可以把主内容区域(main)和侧边栏(aside)定义为容器,在主内容区域和侧边栏放置同一组件.card,而且 .card组件会根据其容器的大小,展示不同的布局(放在侧边栏里的.card是垂直布局,放在主内容区域的.card是水平布局):

/* 显式声明查询容器 */
main, aside {
    container: inline-size;
}

.card {
    display: grid;
    grid-template: 'img' auto 'content' auto / 100%;
}

@container (inline-size > 45em) {
    .card {
        grid-template: 'img content' auto / auto 1fr;
    }
}

注意,我们也可以使用 contain 来声明一个查询容器,比如:

main, aside {
    contain: layout inline-size style;
}

详细的可以阅读《初探CSS容器查询》一文。

container@container

container@containerCSS Containment Module Level 3 新增的两个属性,它们看上去非常相似,但他们有着本质的区别:

  • containercontainer-typecontainer-name 的简写属性,用来显式声明某个元素是一个查询容器,并且定义查询容器的类型(可以由container-type指定)和查询容器的名称(由container-name指定)。
  • @container(带有@规则),它类似于条件CSS中的@media@supports规则,是一个条件组规则,其条件是一个容器查询,它是大小(size)和(或)样式(style)查询的布尔组合。只有当其条件为真(true),@container规则块中的样式都会被用户代理运用,否则将被视为无效,被用户代理忽略。

我们先从 container 开始!

定义一个包含性上下文

要使用CSS容器查询特性首先要做的是 定义一个包含性上下文(Containment Context)。这样可以告诉浏览器以后要针对只个容器进行查询,以及具体如何查询该特定的容器。即,在元素上显式使用 container 属性。比如,上面示例代码:

main,aside {
    container: inline-size
}

其实,containercontainer-typecontainer-name 的简写属性,其语法规则:

container: <container-type> [/<container-name>]?

<container-name> 可以被省略,如果该值被省略,它将被重置为其初始值(initial)。如果<container-name>未被省略,则需要使用反斜杠(/)和前面的<container-type> 分隔开来,并且最好是在/前后有一个空格符。

也就是说,我们可以使用container同时定义容器类型和容器名称:

main {
    container: size / layout;
}

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

上面代码等同于:

main {
    container-type: size;
    container-name: layout;
}

.component {
    container-type: inline-size;
    container-name: card;
}

即:

  • 使用container-type指定查询容器类型,比如示例中的inline-size,会告诉浏览器查询容器的内联轴尺寸
  • 使用container-name给查询容器命名,该命名可以用于@container规则中,比如 @container card (inline-size > 45em){}

接下来,花点时间了解一下container-typecontainer-name 的具体使用以及作用。

指定查询容器类型:container-type

container-type 属性对所选元素创建了一个包含性上下文格式(Containment Context),简单地说,就是创建了一个查询容器。允许其后代元素查询其样式、尺寸和布局等各个方面,并作出相应的反应。

该属性可接受的值有:

container-type: none | style || state || [size | inline-size | block-size]

其中none是其初始值。每个值的含义是:

  • size :在容器的块轴(Block Axis)和内联轴(Inline Axis)上建立一个查询容器,用于尺寸查询,即查询容器块轴和内联轴的尺寸(block-sizeinline-size)。对元素应用布局(layout)、样式(style)和尺寸(size)限制
  • inline-size :在容器的内联轴(Inline Axis)上建立一个查询容器,用于内联轴尺寸(inline-size)查询。对元素应用布局(layout)、样式(style)和内联轴尺寸(inline-size)的限制
  • block-size :在容器的块轴(Block Axis)上建立一个查询容器,用于块轴尺寸(block-size)查询。对元素应用布局(layout)、样式(style)和块轴尺寸(block-size)的限制
  • style :建立一个查询容器,用于样式查询
  • state :建立一个查询容器,用于状态查询

比如下面这个示例,开发者可以创建一个查询容器,并且让容器后代元素根据查询容器的内联轴尺寸的大不来调整font-sizeline-height 和 其他样式,创建一个响应容器的效果:

<!-- HTML -->
<main>
    <h2> Container Queries</h2>
</main>

/* CSS */

/* 创建一个查询容器*/
main {
    container-type: inline-size;
}

h2 {
    font-size: 2rem;
    color: orange;
}

@container (width >= 40em) {
    h2 {
        font-size: 3rem;
        color: lime;
    }
}

查询条件使用的 width >= 40em,也就是说,当查询容器宽度大于40em时,其后代元素 h2font-sizecolor就会做出变化的响应。

拖动虚线框右下角滑块,改变main容器(它是一个查询容器)的width,你将看到下面这样的效果:

特别声明,示例中用于@container的查询条件(width >= 40em) 相当于 (min-width: 40em)

Media Queries Level 4 开始, 在@media 规则中,可以使用我们熟悉的数学表达式,比如>=<=等来替代以往不易于理解的min-max-

比如:

@media (max-width: 320px) { 

}
@media (min-width: 321px) { 

}

// 等同

@media (width <= 320px) { 

}
@media (width >= 321px) { 

}

不过,目前还需要借助 PostCSS插件(postcss-media-minmax)才能这样使用。你还可以使用另一个PostCSS插件(postcss-custom-media)来使用自定义媒体查询。我亲测了一下,@container 中还不能使用 <=这样的表达式。因此上面示例中的代码,要换成下面这样的才能正常看到效果:

@container (width >= 40em) {
    h2 {
        font-size: 3rem;
        color: lime;
    }
}

/* 等同 */
@container (min-width: 40em) {
    h2 {
        font-size: 3rem;
        color: lime;
    }
}

我们了可以使用 container-type: size向浏览器表明,查询容器的大小在两个维度上都是已知的。然而,我们往往不知道东西在两个维度上有多大。当我们使用媒体查询时,大多数时候关心的是可用宽度(或内联尺寸)。我们将列定义为该维度空间的百分比或分数。因此,容器查询可以使用容器类型属性来允许只在一个维度上表示尺寸。这种情况也被称为单轴(可以是inline-size,也可以是block-size)包含。但这并不意示着我们只能使用单轴包含。有时候双维的改变也可以实现一些不一样的效果。比如 @JheyCodepen 上写的案例,就是两个维度改变查询容器尺寸(size)。我Fork了一份,关键代码如下:

t-shirt path {
    fill: hsl(var(--hue, 10), 60%, 65%);
    transform: scaleX(var(--width, 1)) scaleY(var(--length, 1));
}

.t-shirt__badge:after {
    content: var(--size, "S");
}

.t-shirt__container {
    transform: translate(-50%, -50%) scale(var(--scale, 1)) scaleX(1);
}

/* 容器查询 */
.container {
    container-type: size;
}

@container (min-width: 300px) and (min-height: 300px) {
    .t-shirt__container {
        --scale: 1.5;
        --size: "M";
        --hue: 210;
    }
}

@container (min-width: 400px) and (min-height: 400px) {
    .t-shirt__container {
        --scale: 2;
        --size: "L";
        --hue: 104;
    }
}

@container (min-width: 500px) and (min-height: 500px) {
    .t-shirt__container {
        --scale: 2.5;
        --size: "XL";
        --hue: 280;
    }
}

效果如下:

拖动示例中右下角滑块,同时改为查询容器两个维度尺寸(widthheight),可以看到衬衫颜色和尺码都会改变:

注意,这里复制@Jhey的Demo,主要是因为他的Demo使用的是contain@container以及CQFill构建的,我们今天主要聊的是 container@container,为此把他示例中的contain: layout size style 换成 container-type: size

除了查询容器尺寸(sizeinline-sizeblok-size)之外,也可以查询其style(计算样式,即 Computed Style)。这对于在多个属性之间的切换行为很有用。比如下面这个示例:

section {
    container-type: style;
}

@container (--cards) {
    article {
        border: thin solid silver;
        border-radius: 0.5em;
        padding: 1em;
    }
}

只不过到目前为止,container-type指定为style还未得到浏览器支持。不过在Github上有专门的Issue在讨论,未来查询容器的计算样式的语法和使用规则。感兴趣的可以查阅《Define a syntax for style-based container queries》中的相关讨论和跟踪!最早有关于这方面的讨论都集中在 Issue #5989Issue #5624!

同样的,container-type 运用state值也还未得到支持,具体的语法规则和使用方式在《Define a syntax for state-based container queries》中讨论,@Mirisuzanne 和 @Argyleink 都提出相关的建议,比如:

@container state(stuck) {
    nav {
        --box-shadow-y-distance: 15px;
    }
}

@container state(in-view) {
    section {
        animation: reveal .5 ease forwards;
    }
}

换句话说,到止前为止,在container-type中只能使用sizeinline-sizeblock-size,即查询容器尺寸。另外,在使用容器查询时,还应该避免与containcontainer的其他用法相混淆。有关于这方面的讨论可以阅读《Bikeshed the container property & at-rule names to avoid confusion with other usage of contain/container》。

特别声明,要了解当你对一个盒子应用layoutstylesize遏制时会发生什么,就需要查看 contain 相关的介绍或者阅读早期整理的《初探CSS的容器模块》。

查询容器的命名:container-name

查询容器是可以被命名的,即, 使用 container-name 属性给查询容器指定一个名称,这个名称就是查询容器的名称。查询容器的名称可以被 @container 规则使用,可以用来过滤哪些查询容器是目标。这个稍后再细说,先来看其语法和值:

container-name: none | [<custom-ident> | <string>]+

none是其初始值。其每人值的作用是:

  • none :不给查询容器命名
  • <custom-ident> :自定义标识符,指定一个查询容器名称作为标识符
  • <string> :将查询容器名称指定为一个<string>值;这将计算为一个与给定<string>值相同的标识符

注意,关键词none和字符串"none"作为<custom-ident><string>值是无效的。

在开始使用 container-name 之前,我们先来体验未命名的查询容器的示例:

<!-- HTML -->
<main>
    <h2> Container Queries</h2>
</main>

<section>
    <h2> Container Queries</h2>
</section>

/* CSS */
main, section {
    container-type: inline-size;
}

h2 {
    font-size: 2rem;
    color: orange;
}

@container (max-width: 40em) {
    h2 {
        font-size: 3rem;
        color: lime;
    }
}

@container (max-width: 40em) {
    h2 {
        font-size: 4rem;
        color: #f36;
        text-shadow: 1px 1px 1px rgb(255 255 255 / .6);
    }
}

拖动示例中虚线边框右下角滑块,效果如下:

你会发现,示例中的两个@container 规则条件是一样的,只是@container 条件块中的样式规则不同,但最终运用于mainsection 的查询是位于源码后面的那个 @container规则。其实,这也遵循了 CSS 级联的规则,因为@container 是两个相同的规则,即使里面样式规则不一样,这样一来位于后者则胜出。

如果你对 CSS 的级联相关的知识感兴趣的话,那么这几篇文章值得你花点时间去阅读:《初探 CSS 的级联层(@layer》、《聊聊CSS中的层叠相关概念》、《管理CSS层叠》 和《图解CSS:CSS层叠和继承》!

如果希望避免这种现象。也就是希望查询指定的查询容器,那么container-name就显得非常有意义了。把上面的示例稍作修改:

main {
    container-type: inline-size;
    container-name: main;
}

section {
    container-type: inline-size;
    container-name: section;
}

h2 {
    font-size: 2rem;
    color: orange;
}

@container main (min-width: 40em) {
    h2 {
        font-size: 3rem;
        color: lime;
    }
}

@container section (min-width: 40em) {
    h2 {
        font-size: 4rem;
        color: #f36;
        text-shadow: 1px 1px 1px rgb(255 255 255 / 0.6);
    }
}

同样拖动示例中虚线框右下角滑块,你会发现mainsection两个查询容器会分别使用mainsection的查询规则中的样式,因此它们的效果不再像上面的示例一样了:

有了查询容器命名的能力之后,在某些情况下,我们想查询一个特定容器的各个方面,即使它不是最近的祖先容器。例如,我们可能想查询一个main容器高度(block-size),以及一个更多嵌套的内联查询容器宽度(inline-size)。

<!-- HTML -->
<main>
    <section>
        <h2>Container Queries</h2>
    </section>
</main>

/* CSS */
main {
    container: block-size / main;
}

section {
    container: inline-size / section;
}

h2 {
    font-size: 2rem;
    color: orange;
}

@container main (min-height: 12em) {
    h2 {
        color: lime;
        font-size: 3rem;
        text-shadow: 1px 1px 1px rgb(255 255 25 / 0.5);
    }
}

@container section (min-width: 30em) {
    h2 {
        color: #f36;
        font-size: 4rem;
        text-shadow: 1px 1px 2px rgb(255 255 25 / 0.5);
    }
}

示例中绿色框容器命名为main查询容器,红色框容器命名为section查询容器,其中main查询容器查询的是block-size(容器高度),section查询容器查询的是inline-size(容器宽度)。

注意,当mainsection两个查询规则都成立(@container的条件为true),位于后面的查询规则胜出(同样遵循级联规则):

由于 container-name 允许使用<string>值,所以我们也可以使用attr()函数,从HTML元素的属性中生成查询容器名称,比如:

<!-- HTML -->
<main data-container="main">
    <h2>Container Queries</h2>
</main>

/* CSS */
main {
    container-type: inline-size;
    container-name: attr(data-container);
}

@container main (max-width: 30em) {
    h2 {
        color: red;
    }
}

查询容器的查询特征

查询容器的查询物征是指可以查询一个查询容器的特定方面。

到目前为止,在《What container features can be queried? 》中正在讨论哪些容器特征可以被查询。在 CSS Containment Module Level 3 规范中也有专门的一节内容在介绍查询容器可被查询特征。

简单地说,查询容器的 sizestylestate 等特征都要以被查询,但到目前为止,container-type 使用stylestate属性值还未得到浏览器的支持,所以我们今天只聊size方面的查询特征。

在前面向大家演示的示例,都是在查询容器的盒子尺寸,即宽度(inline-size)和高度(block-size),或者同时查询容器宽高(size)。我们把这些都被称为 尺寸查询<size-query>),允许查询查询容器的主体尺寸。它是单尺寸特征(<size-feature>)的布尔组合,每个特征都查询查询容器的单个特定尺寸特征。<size-feature>的语法与媒体特征的语法相同:一个特征名、一个比较器和一个值

如果查询容器没有主体盒子,或者主体盒子不是布局包含框,或者查询容器不支持相关轴的尺寸查询,那么评估尺寸特征的结果是未知的。

使用size查询查询容器的方式(查询容器特征)主要有:

  • width :查询的是查询容器内容框(Content Box)的宽度
  • height :查询的是查询容器内容框的高度
  • inline-size :查询的是查询容器的内容框在查询容器的内联轴中的尺寸
  • block-size :查询的是查询容器的内容框在查询容器的块轴的尺寸
  • aspect-ratio :被定义为宽度容器特征的值与高度容器特征的值的比率
  • orientation :它分纵向(portrait)和 横向(landscape),其中纵向是指当高度容器特征的值大于或等宽度容器特征的值是,方向容器特征为缘纵向;否则为横向

不幸运的是,到目前为止,我亲测了一下,目前也只有widthheight查询特征被支持。也就是在@container规则中的max-widthmax-heightmin-widthmin-height。这个和媒体查询是相似的,在未来也可以使用 >=<= 以及它们的组合方式来描述,比如 @container (width >-= 20em)

<size-query> 除了包含 <size-feature> 之外,还包含<size-condition>

<size-condition> = not ( <size-query> ) | ( <size-query> ) [ and ( <size-query> ) ]* | ( <size-query> ) [ or  ( <size-query> ) ]*

<size-feature> 可以和 与(and或(or、**非(not)**组合使用,比如:

@container not (max-width: 30em) {

}

@container (min-width: 300px) and (min-height: 300px) {

}

@container (min-width: 300px) or (min-height: 300px) {
    
}

注意,这个和 CSS的 @media@supports使用相似,具体介绍可以阅读《图解CSS:条件 CSS》和《CSS3条件判断:@supports》!

不过,容器查询中有个细节需要注意了。我们 在容器查询条件中会使用相对单位,比如emremvw,这些相对单位的计算方式与 CSS 媒体查询中的相对单位处理有所不同。容器查询条件中的相对单位是根据查询容器的计算值来计算的,比如em,但也不是千篇一律的,比如视窗单儀 vwrem

特别声明,在容器查询条件中不能使用百分比单位(%),这一点和CSS媒体查询条件中不能使用百分比单位是相同的!有关于 CSS 单位和值更多的介绍可以阅读《图解CSS:CSS 的值和单位》一文。另外,你要是想了解CSS中百分比单位计算方式,可以阅读《CSS中百分比单位计算方式》一文。

为了能更好的让大家理解这一点,我们来看规范中提供的一个em的示例:

aside, main {
    container-type: inline-size;
}

aside { 
    font-size: 16px; 
}

main { 
    font-size: 24px; 
}

@container (width > 40em) {
    h2 { 
        font-size: 1.5em; 
    }
}

示例中查询条件使用的是em单位,而且不同查询容器的font-size也不同。这个时候,查询容器将相对于它们自己的font-size来计算em的值,即查询条件中使用的 40em 值是相对于自身查询容器上的font-size计算:

  • 对于aside查询容器,是相对于16px计算,查询条件将在查询容器宽度大于 640px40 x 16 = 640px)为真(true
  • 对于main查询容器,是相对于24px计算,查询条件将在查询容器宽度大于960px40 x 24 = 960px)为真(true

案例

在《初探CSS容器查询》一文中,我们使用的是contain@container构建的了一个卡片组件,可以根据容器宽度尺寸调整不同的布局和UI效果。你可以把文中的案例将声明包含上下文的代码使用container来替换,即:

.container {
    contain: layout inline-size style;
}

/*替换成*/
.container {
    container-type: inline-size
}

其他代码不变,就OK了。

不过,今天我们不拿以前的案例来举例子。我们拿 @Una Kravets 在 Codepen上的案例来举例,因为这个案例有一个特点,查询容器嵌套:

把@Una Kravets 的示例 Fork 一份过来,稍作调整。关键代码如下:

.cart-button-container {
    container: inline-size;
}

.cart-icon {
    container: inline-size;
}

/* Cart lines at 30px or wider*/
@container (min-width: 30px) {
    .cart-lines-group {
        display: block;
    }
}

/* Add "+" at 50px or wider*/
@container (min-width: 50px) {
    .plus-group {
        display: block;
    }
}

/* "Add" instead of "+" at 100px*/
@container (min-width: 100px) {
    .cart-button {
        padding: 0 1rem;
        display: grid;
        max-width: 120px;
        grid-template-columns: 1fr 1fr;
    }

    .plus-group {
        display: none;
    }

    .cart-text .add {
        display: inline-block;
    }

    .cart-icon {
        margin-right: 0;
    }
}

/* "Add to cart" for wider spaces*/
@container (min-width: 220px) {
    .cart-button {
        max-width: 260px;
        grid-template-columns: 1fr 3fr;
    }

    .cart-text .to-cart {
        display: inline-block;
    }
}

.product-card-container {
    container: inline-size;
    container-name: product-card-container;
}

.product-card .cart-button {
    container-name: product-card-container;
}

@container (min-width: 200px) {
    .product-card .desc {
        display: block;
    }

    .product-card {
        padding: 1rem 1rem 2rem;
        border: 5px solid var(--btn-bg);
        text-align: left;
    }

    .product-card .title {
        font-size: 1.25rem;
    }

    .product-card .price {
        font-size: 1rem;
    }
}

@container (min-width: 250px) {
    .product-card {
        border: 11px solid var(--btn-bg);
    }

    .product-card .title {
        font-size: 1.5rem;
    }

    .product-card .price {
        font-size: 1.25rem;
    }
}

@container product-card-container (min-width: 400px) {
    .product-card {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: center;
        gap: 2rem;
    }

    .product-card .cart-button {
        margin: 0;
    }
}

@supports (container: inline-size) {
    .warning {
        display: none;
    }
}

拖动示例中滑块,改变容器宽度,你将看到的效果如下:

小结

结合上一篇《初探CSS容器查询》的介绍,在CSS中有两种方式可以实现容器查询的效果。第一种是 contain@container,第二种就是今天这篇文章介绍的container@container。我们可以使用container以及他的子属性container-typecontainer-name 创建查询容器,然后使用@container规则来查询容器后代元素设置样式,实现UI根据查询容器尺寸响应变化的效果。

到目前为止,我们只能查询查询容器的尺寸sizeinline-sizeblock-size),在未来,我们可能还可以查询查询容器的stylestate。那么未来可用的查询特征就会越来越多。另外@container中的规则还只能使用max-widthmax-heightmin-widthmin-height,同样的,在未来这些查询条件可以像媒体查询一样,可查询的条件会越来越多。让我一起来期待吧!