CSS的if()函数正式落地,写样式做条件判断还怕啥?

3,438字
15–22 分钟
in

这波真的快,Chrome 137版本刚把CSS的if()函数正式上线。从CSS工作小组拍板到能用上,前后不到一年,搁以前这种骚操作不得等个三五年?现在好了,样式表里也能搞条件判断,不用再羡慕JS那些三元运算符。不过话说回来,if()到底是个啥玩意?简单讲,它就是内联在CSS属性值里的条件函数,允许对某个属性根据不同的条件输出不同的值。好比说“如果主题是深色模式,内边距给2rem,否则给3rem”——一行代码搞定,不用写一堆媒体查询或者JS去切class。

目录

条件判断在CSS里其实遍地都是,选择器本身就是条件,@media@supports也是。但以前没法把条件判断塞进一行属性值里。if()函数填补了这个空白,它把style()media()supports()这些判断包装起来,然后返回对应的结果。Chrome 137+已经支持,其他浏览器估计也快跟上,趁热先玩起来。

语法长啥样

if()的写法看着唬人,拆开看其实就三板斧。基本骨架是这样:

属性名: if(
  条件1: 结果1;
  条件2: 结果2;
  else: 默认结果
);

每个条件要用style()media()supports()包起来,结尾用分号隔开。最后一个else兜底,前面所有条件都不命中时就用它。看个最简版本:

.box {
  padding: if(style(--theme: dark): 2rem; else: 3rem);
}

这段代码的意思是:检测--theme这个自定义属性是不是等于dark,是的话内边距给2rem,不是的话给3rem。初次上手可能会忘记末尾的分号,或者把else写成else:后面漏空格,这都会让整条规则直接挂掉。写的时候记得每个分支后面跟一个分号,else后面也要跟冒号和值,和普通分支一样对待。

多条件咋写

一个条件不够用?那就多叠几个。比如想根据不同的高度变量来改变元素的实际高度:

:root {
  --size-a: 3rem;
  --size-b: 7rem;
  --size-c: 10rem;
  --default-h: 5rem;
}

.card {
  height: if(
    style(--size-a: 3rem): 14.5rem;
    style(--size-b: 7rem): 10rem;
    style(--size-c: 10rem): 2rem;
    else: var(--default-h)
  );
}

条件多了之后,挤在一行简直瞎眼。推荐像上面那样换行缩进写,每个条件独占一行,谁跟谁一眼就能分清。另外要注意,条件是从上往下匹配的,命中第一个之后就不会再看后面的了。所以得把最特殊的条件放在最前面,通用的放后面。比如--size-c匹配范围更窄,就放前头;else永远放最后当备胎。

还有一种翻车情况:自定义变量压根没定义。这时候style()比较的结果是false,会直接跳到下一个条件或else。所以最好在:root里给变量一个保底值,或者靠else兜住。

media咋配合

响应式字体大小以前得写三坨@media,现在一个if()全包圆:

h1 {
  font-size: if(
    media(width >= 1200px): 3rem;
    media(width >= 768px): 2.5rem;
    else: 2rem
  );
}

media()里可以直接写width >= 1200px这种简写,不用再写(min-width: 1200px)那套老语法。但得注意,老的媒体条件比如(min-width: 768px)也兼容,不过新写法更短,推荐用起来。这里有个坑:media()不支持复杂的and/or嵌套?目前规范里允许<media-condition>,所以media((width >= 768px) and (orientation: landscape))这种也是可行的。但刚上线的实现可能还不稳,最好先测一下。

实际开发中,如果同时有多个媒体条件和样式条件混着写,顺序一样很重要。比如想优先判断屏幕尺寸,就把media()放前面,style()放后面。

特性检测也能内联

@supports能干的活,supports()也能干,只不过返回值而不是代码块。举个例子,想给卡片加背景模糊,但怕老浏览器不认backdrop-filter

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    else: unset
  );
  background-color: oklch(20% 50% 40% / 0.8);
}

这比写两遍.card规则要清爽多了。如果还想检测多个特性,就继续往上堆分支:

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    supports(backdrop-filter: invert(50%)): invert(50%);
    supports(backdrop-filter: hue-rotate(230deg)): hue-rotate(230deg);
    else: unset
  );
}

supports()检测的时候,注意属性值要写完整,比如backdrop-filter: blur(10px)。如果写成blur(10px)缺了属性名,条件永远不成立。另外,不同浏览器对某些新属性的支持程度不一样,supports()只能检测语法上是否支持,不能检测运行时bug。所以else分支最好写一个降级方案,比如unset或者更保守的样式。

格式化防眼瞎

写三个以上条件的时候,代码很容易变得像天书。除了换行缩进,还可以把自定义变量统一收在:root里,避免魔法数字散落各地。另外,每个分支的结果值尽量用变量或者有语义的尺寸,别写一堆裸数字。看个对比:

/* 这样写,三天后自己都不认识 */
.element {
  margin: if(style(--mode: compact): 4px 8px; style(--mode: wide): 12px 24px; style(--mode: cozy): 8px 16px; else: 6px 12px);
}

/* 这样写,同事看了都说好 */
:root {
  --margin-compact: 4px 8px;
  --margin-wide: 12px 24px;
  --margin-cozy: 8px 16px;
  --margin-base: 6px 12px;
}
.element {
  margin: if(
    style(--mode: compact): var(--margin-compact);
    style(--mode: wide): var(--margin-wide);
    style(--mode: cozy): var(--margin-cozy);
    else: var(--margin-base)
  );
}

还有一个容易忽略的点:if()里面每个分支的结果值不能包含顶层逗号或分号(除非被括号或函数包着),否则会提前截断。比如想输出rgba(0,0,0,0.5)没问题,但想输出1, 2, 3就不行,因为逗号会被解析成分隔符。解决办法是把多值的东西塞进自定义变量里,或者用--变量中转一下。

当前能用的姿势

写这篇文章的时候,if()只在Chrome 137及以上版本跑得动。想在生产环境用,得搭配@supports做渐进增强:

/* 后备样式 */
.box {
  padding: 3rem;
}

/* 支持if()的浏览器会覆盖 */
@supports (padding: if(style(--test: 1): 1px; else: 2px)) {
  .box {
    padding: if(style(--theme: dark): 2rem; else: 3rem);
  }
}

检测if()本身是否支持有点绕,因为@supports里直接写if()语法可能报错。上面用了取巧的方式:在一个已知支持的属性里塞if()表达式,看浏览器认不认。如果认,整个@supports块就会生效。这招目前实测可用,但未来规范可能有变动,多留意浏览器更新日志。

浏览器最低版本稳定程度
Chrome137已支持
Edge137已支持
Firefox未知开发中
Safari未知未表态

想现在就玩起来?下个Chrome Canary或者直接升到137稳定版,打开控制台就能试。拿不准的地方多写几个分支跑一跑,翻车了就改else兜底。这玩意儿真·上头,以后写响应式、主题切换、特性检测都不用到处贴@media@supports了,一行if()解决战斗。