容器查询与容器状态查询

容器状态查询是 2023 年的新特性,后续又推出了容器的滚动状态查询,这对于滚动文档下的某些辅助功能大有裨益——例如滚动吸附状态、是否能滚动到顶部、是否可以横向滚动等。

以前自动样式是根据屏幕大小切换,现在是根据父容器大小自动切换样式。容器查询是媒体查询(Media Queries)以来,响应式设计的最大变革。

容器查询类似于媒体查询,是一个 @ 规则(at-rule),意味着它是一个条件规则:如果满足条件,会应用内部的 CSS 层叠样式。

参考资料

  1. Container Scroll-state Queries - MDN
  2. CSS 容器查询 - MDN
  3. CSS Scroll-state - Chrome for Developers

前言

容器状态查询需要一个可滚动的上下文,即滚动容器环境,容器提供查询点。有时滚动容器希望查询自身状态并应用样式规则;有时,子元素需要获得容器与自己的关联状态——比如子元素在滚动容器内发生粘贴定位(sticky)时,或在滚动容器内发生吸附(snap)时,应用规则设置自身的样式。

容器查询环境(上下文)

所谓滚动查询状态,即上文提到的可滚动、发生滚动、吸附、粘贴,都需要在一个可以滚动的上下文语境中,即存在一个滚动的容器。除了滚动容器可以查询自身的滚动状态外,子元素可能依赖滚动容器产生吸附(scroll-snap-align1 或粘贴定位(position: sticky2 行为。这些状态都依赖一个滚动的上下文。

容器状态关键点

容器查询语句块本身依赖 container-type: scroll-state CSS 规则的设置。换句话说,container-type 属性值包含 scroll-state,即表明该元素可以使用滚动状态查询。

滚动查询基本语法:

@container [<container-name>] scroll-state(scrollable | scrolled | snapped | stuck: <keyword>)

当 CSS 规则被设置为 container-type: scroll-state 的元素被当作滚动上下文的容器角色时,其可用的状态查询包含 scrollablescrolled

  • scrollable:查询是否可以滚动到滚动轴的某一个端点
  • scrolled:标识当前是否沿着滚动轴的某一个方向发生了滚动行为

container-type: scroll-state CSS 规则被用于滚动容器内的子元素时,可以使用的值包括 snappedstuck

  • snapped:表明该元素除了 container-type: scroll-state 规则外,还在滚动上下文中使用了滚动吸附效果。即本元素还至少设置了 scroll-snap-align,其某个可滚动的父元素还设置了 scroll-snap-type 属性。这是此状态查询的上下文必须满足的条件,否则将不生效。

  • stuck:表明该元素除了设置了 container-type: scroll-state CSS 规则外,自身至少还设置了 position: sticky CSS 规则,表明其自身在一个可滚动上下文中会产生粘贴行为。这是要应用此规则查询的基本要求,否则此 @ 规则将不会生效。

使用 scrollable 查询

scrollable 如上文所述,用于查询滚动容器自身的滚动状态。

因为要查询自身状态,因此其自身至少是一个可滚动容器,其 container-type 属性也必须是 scroll-state,这样就标记其可以用于 @container at-rule。如下所示:

.container {
  overflow-y: auto;
  container-type: scroll-state;
  container-name: my-container;
}

提示:以上还额外设置了 container-name,这不是必须的,但如果未来 CSS 代码越来越复杂时,可以使用该名称作为容器查询的指定查询对象。即容器状态查询可以指定该容器名称应用规则。

查询基本语法示例:

@container my-container scroll-state(scrollable: top) {
  /* CSS rules go here */
}

这里,我们只查询名为 my-container 的容器,以确定该容器是否可以滚动到其顶部边缘。

注意 my-container 部分是可选的。假设不设置,则该查询的内部规则将应用到页面上的所有可滚动容器中,就像媒体查询一样。

其中查询中的 top 就是标识需要查询的方向,可以是:

  • 物理值topleftbottomright
  • 逻辑值block-endinline-startinline-end
  • 滚动轴xyinlineblock

案例

在 HTML 代码中,我们有一个 <article> 元素,其中包含足够的内容使得文档可以滚动,元素前面还有一个返回顶部的链接:

<a class="back-to-top" href="#" aria-label="Top of page">↑</a>

<article>
  <h1>Reader with container query-controlled "back-to-top" link</h1>
  <section>
    <header>
      <h2>This first section is interesting</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </header>
    ...
  </section>
  ...
</article>

为了简洁起见,隐藏了大部分 HTML 代码。

CSS 部分,链接 .back-to-top 被放置在视口的右下角,并使用规则 translate: 80px 0; 将其移出视口。当 translate 任一值发生变化时,该值将使链接和元素呈现动画效果。

.back-to-top {
  width: 64px;
  height: 64px;
  color: white;
  text-align: center;
  position: fixed;
  bottom: 10px;
  right: 10px;
  translate: 80px 0;
  transition:
    0.4s translate,
    0.2s background-color;
}

本例中的滚动容器就是 <html> 元素本身,它被标记为滚动状态查询容器,其 container-type 值为 scroll-state。虽然 container-name 并非绝对必要,但在代码库中添加多个针对不同查询的滚动状态查询容器时,它非常有用。

html {
  container-type: scroll-state;
  container-name: scroller;
}

接下来,定义一个 @container 代码块,用于设置此查询的目标容器名称以及查询本身 scrollable: top。此查询仅在元素可以滚动到其顶部边缘时才应用代码块中包含的规则——换句话说,如果容器之前已向下滚动。如果满足此条件,则会对链接 .back-to-top 应用规则 translate: 0 0,使其重新显示在屏幕上。

@container scroller scroll-state(scrollable: top) {
  .back-to-top {
    translate: 0 0;
  }
}

尝试向下滚动文档,并注意「返回顶部」链接是如何出现的——它会由于 transition 从视口的右侧平滑地动画显示出来。如果您通过激活链接或手动滚动返回顶部,「返回顶部」链接会移出屏幕。


使用 scrolled 查询

如前言中提到的,scrolled 仍然是应用于滚动元素自身的。其基本查询语法如下:

@container [<container-name>] scroll-state(scrolled: <keyword>)

keyword 部分包含的 keyword 就是标识需要查询的方向,可以是:

  • 物理值topleftbottomright
  • 逻辑值block-endinline-startinline-end
  • 滚动方向xyinlineblock
  • 特殊值none,表明查询自身其是否为滚动容器

如果你查询了 @container scroll-state(scrolled: top),测试当前是否按照垂直滚动的顶部方向发生过滚动行为。如果测试结果为真,那么将应用内部的规则到所有滚动容器的所有子元素

警告 请注意,容器查询并不能应用样式到容器自身,如媒体查询一样。

案例

来看一个滚动容器的例子,scrolled 查询仅在用户向上或向下滚动时才分别显示顶部和底部内容「栏」。

在 HTML 中,有一个 <article> 包含足够内容的元素,可以使文档滚动,该元素前面还有两个 <div> 元素,分别代表顶部和底部的「栏」:

<div class="bar" id="top-bar">You're currently scrolling towards the top.</div>
<div class="bar" id="bottom-bar">You're currently scrolling towards the bottom.</div>

<article>
  <h1>Document with scrolled container query</h1>
  <section>
    <header>
      <h2>This first section is interesting</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </header>
    ...
  </section>
  ...
</article>

为了简洁起见,此处隐藏了大部分 HTML 代码。

这里给这些栏简单设置一些 CSS 样式:

.bar {
  border-radius: 10px;
  border: 1px solid #000;
  background-color: #0009;
  padding: 10px;
  color: white;
  text-shadow: 1px 1px 1px black;
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  left: 5px;
  right: 5px;
}

这些「栏」被赋予了一些基本的样式。最重要的是,它们被赋予了 position: fixed 值,我们使用 leftright 值从两侧进行偏移。

接下来,我们为顶部和底部栏设置负 top 值和 bottom 值,使它们默认情况下隐藏在视口上方和下方。我们添加了一个动画效果,当它们的值发生变化时,它们会平滑地显示出来。

#top-bar {
  top: -50px;
  transition: 0.6s translate;
}

#bottom-bar {
  bottom: -50px;
  transition: 0.6s translate;
}

本例中的滚动容器就是 <html> 元素本身,它被标记为滚动状态查询容器,其 container-type 值为 scroll-state。虽然 container-name 并非绝对必要,但当代码库中有多个针对不同查询的滚动状态查询容器时,它就很有用了。

html {
  container-type: scroll-state;
  container-name: scroller;
}

接下来,我们定义两个 @container 代码块,它们都指向 scroller 容器名称。第一个代码块定义一个查询 scrolled: block-end,第二个代码块定义另一个查询 scrolled: block-start。这两个查询分别仅在元素最近滚动到其代码块的起始边缘或结束边缘时应用其代码块中包含的规则——换句话说,当容器向下或向上滚动时。

当任一条件为真时,代码块中引用的进度条的值将被 translate 设置为使其在屏幕上显示。而 @container 条件不再为真的进度条则会移出屏幕。

@container scroller scroll-state(scrolled: block-start) {
  #top-bar {
    translate: 0 55px;
  }
}

@container scroller scroll-state(scrolled: block-end) {
  #bottom-bar {
    translate: 0 -55px;
  }
}

渲染效果如下:


使用 snapped 查询

snapped 通常用于一个滚动容器开启了滚动吸附的父容器内部,且元素自身设置了 scroll-snap-align 的情况下,元素用于查询自身在滚动的父容器中正在被吸附。

其基本语法如下:

@container [<container-name>] scroll-state(snapped: <keyword>) {
  /* rule set */
}

keyword 部分包含的 keyword 就是标识需要查询的方向,可选值包含:xyinlineblock,也可以为 none,匹配没有滚动祖先容器的元素。

如果状态查询条件满足,则将 @container 所表示的代码块内的规则应用于其元素的后代节点(子元素)中。

案例

<main>
  <section>
    <div class="wrapper">
      <h2>Section 1</h2>
    </div>
  </section>
  ...
</main>

为了简洁起见,隐藏了大部分 HTML 代码。

这里为 <main> 元素设置了 overflow: scroll 和固定高度属性,将其转换为垂直滚动容器。我们还设置了 scroll-snap-type: y mandatory; 属性值,将其转换为滚动吸附容器,吸附目标将沿 y 轴吸附到该容器上;属性值 mandatory 表示吸附目标将始终吸附到该容器上。

main {
  overflow: scroll;
  scroll-snap-type: y mandatory;
  height: 450px;
  width: 250px;
  border: 3px solid black;
}

通过设置一个非零值,可以将元素 <section> 指定为捕捉目标 scroll-snap-align。该 center 值表示它们将以其中心点捕捉到容器中。

section {
  font-family: "Helvetica", "Arial", sans-serif;
  width: 150px;
  height: 150px;
  margin: 50px auto;
  scroll-snap-align: center;
}

我们希望启用 <section> 元素容器滚动状态查询功能。具体来说,我们希望测试 <section> 元素是否正在向其容器吸附点对齐,因此我们通过设置 container-typescroll-state 将它们标记为滚动状态查询容器。我们还为它们添加了一个属性 container-name,这并非绝对必要,但如果我们的代码以后变得更加复杂,并且我们需要使用不同的查询来定位多个滚动状态查询容器,那么这个属性将非常有用。

section {
  container-type: scroll-state;
  container-name: snap-container;
}

接下来,我们定义一个 @container 查询规则块,该块设置我们在此查询中要定位的容器名称 snap-container,以及其是否被吸附到父容器的 y 轴上。如果是这种情况,规则将会为 <section> 元素的子元素 .wrapper <div> 应用新的背景和颜色以突出显示它。

@container snap-container scroll-state(snapped: y) {
  .wrapper {
    background: purple;
    color: white;
  }
}

渲染结果如下所示。尝试上下滚动容器,并注意当 <section> 与容器对齐时,其样式如何变化。


使用 stuck 查询

stuck 通常用于一个滚动容器内部,且元素自身设置了 position: sticky 的情况下,元素用于查询自身在滚动的父容器中正在粘贴到其设置的边缘之上。

其基本语法如下:

@container [<container-name>] scroll-state(stuck: <keyword>) {
  /* rule set */
}

keyword 部分包含的 keyword 就是标识需要查询的方向,可以是:

  • 物理值topleftbottomright
  • 逻辑值block-endinline-startinline-end
  • 特殊值none,标识查询自身是否与滚动的父容器任何边产生粘连

如果状态查询条件满足,则将 @container 所表示的代码块内的规则表应用于其元素的后代节点(子元素)中。

案例

来看一个例子:一个滚动容器的内容溢出,position: sticky 当滚动到顶部时,标题会固定在容器的顶部边缘。我们将使用 stuck 滚动状态查询,在标题固定在顶部边缘时,为其设置不同的样式。

在 HTML 中,我们有一个 <article> 元素,其中包含足够的内容,足以使文档滚动。它由多个 <section> 元素构成,每个元素都包含一个带有嵌套内容的 <header> 元素:

<article>
  <h1>Sticky reader with scroll-state container query</h1>
  
  <section>
    <header>
      <h2>This first section is interesting</h2>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </header>
    ...
  </section>

  <section>
    <header>
      <h2>This one, not so much</h2>
      <p>Confecta res esset.</p>
    </header>
    ...
  </section>
  ...
</article>

为了简洁起见,此处隐藏了大部分 HTML 代码。

每个 <header> 元素都有一个 positionsticky 和一个 top0,这两个值会将它们固定在滚动容器的顶部边缘。为了测试 <header> 元素是否固定在容器顶部边缘,它们被标记为滚动状态查询容器,其 container-type 值为 scroll-state。虽然 container-name 并非绝对必要,但如果将此代码添加到包含多个针对不同查询的滚动状态查询容器的代码库中,则此标记将非常有用。

header {
  background: white;
  position: sticky;
  top: 0;
  container-type: scroll-state;
  container-name: sticky-heading;
}

此处还为 <header> 元素内的 <h2><p> 元素添加了一些基本样式和一个 transition 值,这样当它们的 background 值发生变化时,动画效果会更加流畅。

h2,
header p {
  margin: 0;
  transition: 0.4s background;
}

h2 {
  padding: 20px 5px;
  margin-bottom: 10px;
}

header p {
  font-style: italic;
  padding: 10px 5px;
}

接下来,我们定义一个 @container 块,该块设置我们在此查询中针对的容器名称以及查询本身——stuck: top。此查询仅在 <header> 元素被固定到其滚动容器的顶部时,才应用块内包含的规则。在这种情况下,会对包含的 <h2><p> 应用不同的背景和阴影。

@container sticky-heading scroll-state(stuck: top) {
  h2,
  p {
    background: pink;
    box-shadow: 0 5px 2px #00000077;
  }
}

渲染结果如下:

Footnotes

  1. 滚动吸附

  2. 滚动粘贴