CSS 新规范解读: if() 函数、跨文档视图过渡、锚点定位,带你了解 CSS

317 阅读14分钟

今年 6 月 11 日至 13 日,CSS 工作组 (CSSWG) 在西班牙科鲁尼亚举行了今年的第二次线下会议,会议议程很长,讨论了 CSS 语言即将推出的新功能和改进。如果说 2023 年给我们带来了像嵌套、容器和样式查询,以及 has: 选择器这样的重大改进,那么 2024 年将会有更多突破性的功能。无论是像内联条件语句这样刚刚起步的新功能,还是长期项目的收尾工作,2024 年已经充满了令人兴奋的进展——而现在才 7 月!

我想分享一些我认为最有趣和最重要的 CSS 新功能,这些功能在会议中得到了讨论。不过,这篇文章不是对讨论内容的精确回顾,而是对 CSS 新规范的概括性介绍,因为这些规范很新,所以在会议中肯定会讨论。还有一些功能我们没有时间在这篇文章中回顾,因为它们仍在讨论中(例如,砌体布局)。

你可以查看 CSSWG 会议议程,了解具体的讨论议题。

if() 函数:条件样式的曙光

自从 2016 年左右 CSS 自定义属性得到可靠支持以来,开发者们一直在尝试根据自定义属性的值来应用不同的样式,当然,这一切都不需要借助 JavaScript。最早的条件样式解决方案之一是 Roman Komarov 在 2016 年发表的 “CSS 变量的条件”。从那时起,许多其他的“黑科技”被记录下来,用于在 CSS 中实现条件声明(包括 Ana Tudor 在 CSS-Tricks 上发表的这个非常巧妙的技巧)。事实上,你可以在 CSSWG 成员 Lea Verou 最近发表的文章 “CSS 中的内联条件语句,现在?” 中找到一个完整的列表,其中讨论并比较了这些解决方案。

可以肯定的是,开发者们一直渴望使用自定义属性来实现条件样式。现在,我们有了一个可以完成这项任务的 样式查询 规范,但它有一些限制,不仅仅是 浏览器支持 方面的问题。最大的限制是什么呢?我们不能直接为被查询的容器设置样式,所以我们需要在 HTML 中使用一个包装元素。

<div class="news-container" style="--variant: info">
  <p>Here is some good <strong>news</strong></p>
</div>

除了样式查询之外,还需要编写以下代码:

.news-container {
  container-name: news-container;
}

@container news-container style(--variant: info) {
  p {
    color: blue;
    border: 1px solid blue;
  }
}

if() 函数的可能语法

早在 2018 年,CSSWG 就开始 讨论添加 if() 函数。今年 6 月 13 日,也就是六年后,CSSWG 决定开始为 CSS 添加 if() 函数。虽然看起来很美好,但不要指望在至少两年内在浏览器中看到 if() 函数!(这是 Lea 的非官方估计。) 我们可能需要等待更长的时间才能获得足够的浏览器支持,以便在生产环境中可靠地使用它。规范草案才刚刚开始,很多东西都要先通过测试。例如,CSS 变量工作草案始于 2012 年,直到 2016 年才得到广泛的浏览器支持。

从语法上看,if() 函数可能会借用 JavaScript 和其他编程语言中的三元运算符,结构如下:

if(a ? b : c)

其中 a 是我们检查的自定义属性,bc 是可能的条件返回值。要检查样式,可以使用内联 style(--my-property: value)

.forecast {
  background-color: if(style(--weather: clouds) ? var(--clouds-color): var(--default-color));
}

即使 ? 没有在 CSS 中使用,而 : 在其他地方有不同的含义,但我认为这种语法是大多数人都熟悉的,而且它还允许无缝的条件链接。

.forecast {
  background-color: if(
    style(--weather: clouds) ? var(--clouds-color): 
    style(--weather: sunny) ? var(--sunny-color);
    style( --weather: rain) ? var(--rain-color): var(--default-color)
  );
}

if() 函数的未来改进

虽然这些改进可能不会出现在初始版本中,但看看 if() 在未来可能会发生哪些变化还是很有趣的:

  • 支持其他内联条件语句. 我们应该使用 style() 查询来检查自定义属性,但我们也可以使用内联 media() 查询来检查媒体特性,或者使用内联 support() 来检查用户代理是否支持特定属性。
.my-element {
  width: if(media(width > 1200px) ? var(--size-l): var(--size-m));
}
  • 在其他 CSS 函数中使用条件语句. 在未来的草案中,我们可以在其他函数中使用三元运算符,而无需将它们包装在 if() 函数中,例如,就像我们可以在 clamp()round() 函数中进行计算而无需 calc() 函数一样。

跨文档视图过渡:页面间切换更流畅

去年,视图过渡 API 让我们能够在网页和状态之间导航时创建无缝过渡。无需组件或框架,无需动画库——只需要普通的 HTML 和 CSS,再加上一点 JavaScript。视图过渡的第一个实现是在不久前添加到浏览器中的,但它是基于 Chrome 定义的实验性函数,并且仅限于两种状态之间的过渡(单页面视图过渡),不支持不同页面之间的过渡(即 多页面视图过渡),而这正是我们大多数开发者所渴望的。模仿原生应用程序行为的可能性令人兴奋!

这就是为什么 CSS 视图过渡模块 Level 2 如此令人惊叹,也是为什么它是我在这篇文章中介绍的所有 CSS 新功能中最喜欢的一个。是的,该功能带来了页面之间开箱即用的无缝过渡,但真正的好处是它不需要框架来实现。我们不需要使用库——比如 React + 某个路由库——而是可以回归到普通的 CSS 和 JavaScript。

当然,在某些复杂情况下,视图过渡 API 可能无法满足需求,但对于许多我们只想实现页面过渡而不引入框架的性能成本的情况来说,它非常棒。

启用视图过渡

当我们在 同源 的两个页面之间导航时,会触发视图过渡。在这种情况下,导航可能是点击链接、提交表单或使用浏览器按钮前进和后退。相比之下,在同源页面之间使用搜索栏等操作不会触发页面过渡。

我们离开的页面和我们要导航到的页面都需要使用 @view-transition 规则并设置 navigation 属性为 auto 来启用过渡

@view-transition {
  navigation: auto;
}

当两个页面都启用过渡时,浏览器会对两个页面进行“快照”,并平滑地将“之前”页面淡入“之后”页面。

在“快照”之间过渡

你可以看到旧页面如何淡入新页面,这要归功于一整棵新的伪元素树,这些伪元素在过渡期间持续存在,并使用 CSS 动画来产生效果。浏览器会将具有唯一 view-transition-name 属性的元素快照分组,该属性为我们可以引用的过渡设置唯一标识符,并在包含页面上所有过渡的 ::view-transition 伪元素中捕获。

你可以将 ::view-transition 视为所有页面过渡的 :root 元素,将视图过渡的所有部分分组到同一个默认动画中。

::view-transition
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
└─ /* 等等... */

请注意,每个过渡都位于一个 ::view-transition-group 中,该组包含一个 ::view-transition-image-pair,而该 ::view-transition-image-pair 又包含“旧”和“新”页面快照。我们可以根据需要创建任意数量的组,它们都包含一个具有两个快照的图像对。

快速示例:让我们使用 ::view-transition“根”作为参数,选择页面上的所有过渡,并在旧快照和新快照之间创建滑动动画。

@keyframes slide-from-right {
  from {
    transform: translateX(100vw);
  }
}

@keyframes slide-to-left {
  to {
    transform: translateX(-100vw);
  }
}

::view-transition-old(root) {
  animation: 300ms ease-in both slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both slide-from-right;
}

如果我们在页面之间导航,整个 旧页面会向左滑出,而_整个_ 新页面会从右边滑入。但我们可能希望防止页面上的某些元素参与过渡,在页面之间保留它们,而其他所有元素都从“旧”快照移动到“新”快照。

这就是 view-transition-name 属性的关键所在,因为我们可以对某些元素进行快照,并将它们放在自己的 ::view-transition-group 中,与其他所有元素分开,以便单独处理。

nav {
  view-transition-name: navigation;

  /* 
    ::view-transition
    ├─ ::view-transition-group(navigation)
    │  └─ ::view-transition-image-pair(navigation)
    │     ├─ ::view-transition-old(navigation)
    │     └─ ::view-transition-new(navigation)
    └─ other groups...
  */
}

你可以在 GitHub 上 找到一个它的实时演示。需要注意的是,在我写这篇文章的时候,浏览器支持 仅限于 Chromium 浏览器(例如,Chrome、Edge、Opera)。

我们对跨文档视图过渡还有很多期待。例如,如果我们有几个具有不同 view-transition-name 的元素,我们可以给它们一个共享的 view-transition-class 来在一个地方设置它们的动画样式——或者甚至 使用 JavaScript 进一步自定义视图过渡,以检查页面是从哪个 URL 过渡过来的,并相应地设置动画。

锚点定位:更灵活的元素定位方式

在 CSS 中将一个元素相对于另一个元素进行定位,听起来应该是一件很简单的事情,但在现实中,需要根据一系列“魔法数字”来混合使用 inset 属性(topbottomleftright),才能将元素放置在正确的位置。例如,要创建一个在鼠标悬停时在元素左侧弹出的工具提示,在 HTML 中可能看起来像这样:

<p class="text">
  Hover for a surprise
  <span class="tooltip">Surprise! I&#39;m a tooltip</span>
</p>

在 CSS 中,使用当前的方法是这样的:

.text {
  position: relative;
}

.tooltip {
  position: absolute;
  display: none;

  /* 垂直居中 */
  top: 50%;
  transform: translateY(-50%);

  /* 移到左边 */
  right: 100%;
  margin-right: 15px; */
}

.text:hover .tooltip {
  display: block;
}

不得不更改元素的定位和 inset 值并不是世界末日,但确实感觉应该有一种更简单的方法。此外,最后一个例子中的工具提示非常脆弱;如果屏幕太小或我们的元素太靠左,那么工具提示将隐藏或溢出到屏幕边缘之外。

9a1e9fba3b3da69428d2ae9cbf05987.png

CSS 锚点定位是另一个在 CSSWG 会议中讨论的新功能,它承诺让这种事情变得更容易。

创建锚点

基本思路是我们建立两个元素:

  • 一个作为锚点,
  • 另一个是锚定到该元素的“目标”。

这样,我们就有一种更具声明性的方式来关联一个元素并相对于锚定元素定位它。

首先,我们需要使用新的 anchor-name 属性创建锚点元素。

稍微改变一下我们的标记:

<p>
  <span class="anchor">Hover for a surprise</span>
  <span class="tooltip">Surprise! I&#39;m a tooltip</span>
</p>

我们给它一个唯一的 dashed-indent 作为它的值(就像自定义属性一样):

.anchor {
  anchor-name: --tooltip;
}

然后,我们使用 position-anchor 属性将 .tooltip.anchor 关联起来,并使用 fixedabsolute 定位。

.toolip {
  position: fixed;
  position-anchor: --tooltip;
}

.tooltip 目前位于 .anchor 的顶部,但我们应该把它移到其他地方,以防止这种情况。移动 .tooltip 最简单的方法是使用新的 inset-area 属性。假设 .anchor 位于 3×3 网格的中间,我们可以通过给工具提示分配一个行和列来将其放置在网格内。

一个 3×3 的网格,中间的黄色元素被标记为“锚点”。

inset-area 属性接受两个值,用于在网格的特定行和列中放置 .tooltip。它使用物理值,如 leftrighttopbottom,以及根据用户书写模式的逻辑值,如 startend,以及一个共享的 center 值。它还接受引用 x 和 y 坐标的值,如 x-starty-end。所有这些值类型都是表示 3×3 网格上空间的方式。

例如,如果我们想让 .tooltip 相对于锚点的右上角定位,我们可以像这样设置 inset-area 属性:

.toolip {
  /* 物理值 */
  inset-area: top right;

  /* 逻辑值 */
  inset-area: start end;

  /* 混合值! */
  inset-area: top end;
}

最后,如果我们想让工具提示跨越网格的两个区域,我们可以使用 span- 前缀。例如,span-top 将把 .tooltip 放置在网格的顶部和中间区域。如果我们想跨越整个方向,可以使用 span-all 值。

一个 3×3 的网格,中间的黄色元素被标记为“锚点”,周围是三个工具提示示例,演示了如何在网格上放置工具提示,包括每个示例的代码示例。转存失败,建议直接上传图片文件

我们没有使用锚点定位的例子中的一个问题是,工具提示可能会溢出屏幕。我们可以使用另一个新属性 position-try-options 来解决这个问题,并结合新的 inset-area() 函数。

(是的,有 inset-area 属性和 inset-area() 函数。这是我们需要记住的!)

position-try-options 属性接受一个逗号分隔的回退位置列表,供 .tooltip 在溢出屏幕时使用。我们可以提供一个 inset-area() 函数列表,每个函数都包含与 inset-area 属性相同的值。现在,每次工具提示溢出屏幕时,都会“尝试”下一个声明的位置,如果该位置也导致溢出,则尝试下一个声明的位置,依此类推。

.toolip {
  inset-area: top left;
  position-try-options: inset-area(top), inset-area(top right);
}

a60503cf5a8f0106737f1c47336d225.png

这是一个相当复杂的概念,需要一些时间来理解。CSSWG 成员 Miriam Suzanne 与 James Stuckey Weber 坐下来,在一个非常值得观看的视频中讨论并研究了锚点定位。

如果你想了解 TL;DW,Geoff Graham 对视频做了笔记

为了简洁起见,我们这里没有介绍锚点定位的许多方面,特别是新的 anchor() 函数和 @try-position 规则。anchor() 函数返回锚点边缘的计算位置,它提供了对工具提示 inset 属性的更多控制。@try-position 规则用于定义在 position-try-options 属性上设置的自定义位置。

我的直觉是,使用 inset-area 对于绝大多数用例来说已经足够强大。

CSSWG:集体的努力

之前我说过,这篇文章不是对 CSSWG 会议上讨论内容的精确复述,而是对 CSS 新规范的概括性介绍,因为这些规范很新,所以在会议中肯定会讨论。还有一些功能我们没有时间在这篇文章中回顾,因为它们仍在讨论中(例如,砌体布局)。

有一点是肯定的:规范不是在一次或两次会议上凭空产生的;它需要数十位优秀的作者、开发者和用户代理的共同努力,才能将我们在日常 CSS 工作中使用的功能——更不用说我们将来会使用的东西——变成现实。

我也有机会与 CSSWG 的一些优秀开发者交谈,我发现_他们_从会议中得到的最大收获很有趣。你可能认为 if() 函数排在他们列表的首位,因为这正是社交媒体上热议的话题。但是,CSSWG 成员 Emilio Cobos 告诉我,例如,letter-spacing 属性 本质上是有缺陷的,并且没有一个简单的解决方案可以修复它,既要与 CSS 目前定义的 letter-spacing 保持一致,又要与浏览器中使用的方式保持一致。这包括 将普通属性转换为简写属性对代码库来说可能很危险

我们可能认为微不足道的每一个细节,都会为了 Web 和对 Web 的热爱而仔细分析。而且,就像我之前提到的,这些事情不是在封闭的真空中发生的。如果你对 CSS 的未来感兴趣——无论是仅仅关注它还是积极参与其中——那么可以考虑以下任何资源。