写CSS总被条件判断难住,2024年这些新特性真能救场?

3,455字
15–22 分钟
in

从2024年6月的CSS工作组会议透出的风声看,一大波新玩意儿正在路上。像是if()条件函数、跨页面平滑切换、锚点定位这些,要是能早点用上,写样式绝对能少掉不少头发。这篇就掰开揉碎聊聊这几个特性到底能干啥,以及怎么上手试玩。

目录

条件判断if()

啥是if()

写CSS时经常碰到的糟心事儿:想根据某个变量的值换不同颜色,以前要么靠JS,要么搞一堆黑科技。比如用style()查询也能做,但那个只能查容器本身,还得套一层额外标签。现在工作组拍板要搞一个if()函数,就像JS里的三元运算符那样,直接在属性值里写判断。语法大概长这样:if(条件 ? 真值 : 假值)。条件里用style(--变量名: 值)来检查自定义属性的值。

上手玩if()判断

虽然正式进浏览器还得等个两三年(参考CSS变量从草案到普及花了四年),但不妨碍先看看将来怎么写。假设有一块天气预报卡片,背景色要跟着天气变量变:

<div class="forecast" style="--weather: clouds">
  明日多云转阴
</div>
.forecast {
  background-color: if(style(--weather: clouds) ? #b0c4de : #f5f5dc);
}

如果天气选项不止两个,还能链式接下去:

.forecast {
  background-color: if(
    style(--weather: clouds) ? #b0c4de : 
    style(--weather: sunny) ? #ffd700 :
    style(--weather: rain) ? #4682b4 : #f5f5dc
  );
}

这么一写,不用额外套容器也不用写@container规则,属性值本身就带逻辑。比如做深色模式切换时,根据--theme变量直接改按钮颜色,省掉一堆媒体查询套娃。

用的时候注意:条件里style()目前只认自定义属性,想判断媒体宽度(比如if(media(width > 800px) ? ... ))那是以后才可能支持的功能。还有,if()里的问号和冒号两边空格可加可不加,但为了看清楚建议留空格。另外这玩意儿目前连草案初稿都没写完,千万别在生产环境硬上,否则分分钟翻车。

跨文档视图过渡

啥是视图过渡

以前做页面跳转时的平滑动画,要么用React Router外加库,要么自己撸一堆JS监听。去年出来的View Transition API只能管单页内的状态切换(比如点按钮换内容)。现在Level 2搞了个大新闻:两个不同页面之间也能丝滑过渡,而且不需要任何框架。只要两个页面都写上@view-transition { navigation: auto; },浏览器自动拍下旧页面和新页面的快照,然后默认用淡入淡出切换。更骚的是可以自定义每个元素的过渡动画,比如让导航栏单独“粘”在原地不动,其他部分滑动。

让页面切换动起来

先用一个最简单的例子:整个页面左右滑动。在每一个想启用过渡的页面CSS里加上:

@view-transition {
  navigation: auto;
}

然后写动画和伪元素样式:

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

这里root代表整个页面的过渡组。::view-transition-old是旧页面快照,::view-transition-new是新页面快照。两个动画配合起来,旧页向左滑出,新页从右边滑入,绝绝子。

如果不想让页面上的某个元素参与滑动(比如顶栏导航要原地留存),给它单独起个名字:

nav {
  view-transition-name: main-nav;
}

这样浏览器会把这个导航栏单独拍一张快照,放进自己的::view-transition-group(main-nav)里,默认用淡入淡出。要是想自定义它的动画,单独写:

::view-transition-old(main-nav) {
  animation: fade-out 200ms;
}
::view-transition-new(main-nav) {
  animation: fade-in 200ms;
}

实操中要注意:view-transition-name不能重复,同一个页面里不同元素必须用不同名字。而且目前只有Chromium系浏览器(Chrome、Edge、Opera)支持,Firefox和Safari还在憋大招。另外触发过渡的导航必须是同源页面间的跳转,比如点击<a>链接、前进后退按钮;用JS的fetch加载内容再替换DOM那种不算,得额外调用document.startViewTransition()才行。

锚点定位

啥是锚点定位

想把一个提示框贴在另一个元素旁边,以前得用position: relativeabsolute,然后调toprighttransform这些魔术数字。屏幕一变小或者容器位置一变,提示框就可能跑出边界。锚点定位就是专门解决这个痛点的:把一个元素设成“锚”,另一个元素直接说“我要贴着你”,然后用一个3×3的格子网格来摆放位置,超界了还能自动换方向。

用锚点做个不跑偏的提示框

HTML结构:

<p>
  <span class="anchor">鼠标悬停有惊喜</span>
  <span class="tooltip">🎉 惊不惊喜!</span>
</p>

CSS部分:

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

.tooltip {
  position: fixed;
  position-anchor: --surprise;
  inset-area: top right;
  opacity: 0;
  transition: opacity 0.2s;
}

.anchor:hover + .tooltip {
  opacity: 1;
}

解释一下:anchor-name--开头(跟自定义变量一个规矩),把.anchor变成锚点。.tooltipposition-anchor指向那个锚点名字,然后inset-area: top right表示提示框出现在锚点的右上角外面。默认位置是覆盖在锚点正上方,加了inset-area才挪开。

但假如屏幕右边空间不够,提示框就会溢出。这时候加上防溢出套餐:

.tooltip {
  position: fixed;
  position-anchor: --surprise;
  inset-area: top right;
  position-try-options: inset-area(top), inset-area(top left), inset-area(bottom right);
}

position-try-options给了一串备胎位置:先试右上,溢出了就试正上方(inset-area(top)),还溢出就试左上,再不行就试右下。浏览器会自动按顺序尝试直到不溢出为止,比手动写媒体查询省心一万倍。

要注意inset-area属性和inset-area()函数是两个东西,前者直接指定位置,后者用在position-try-options里做回退。另外anchor-name的作用域是同一个父级容器内?其实不是,只要两个元素都在页面上且没有跨影子DOM,就能互相锚定。不过锚点元素必须是块级或行内块级,display: inline的锚点可能定位不准,最好给.anchor加个display: inline-block。还有就是目前锚点定位也在实验阶段,需要Chrome Canary或Edge Dev版本开启#enable-css-anchor-positioning标志才能试玩。

更多好玩但没细说的

这次工作组会议还聊了 masonry 网格布局、letter-spacing 的底层缺陷怎么修这些硬骨头。每一个看似简单的属性背后都是几十号人反复吵架又和好的结果。比如 if() 从2018年有人提到现在才正式立项,视图过渡 Level 2 也是讨论了无数轮跨页面快照的细节。这些新特性进浏览器通常要两到四年,但了解它们的设计思路,至少能提前规划未来的写法。