阅读视图

发现新文章,点击刷新页面。

如何在 CSS 中正确使用 if()

CSS 正在不断进化,而 if() 函数 的引入,就是其中一个非常值得关注的新特性。它让我们可以直接在 CSS 中编写条件逻辑,根据不同状态动态切换样式,从而减少对 JavaScript 的依赖。

if() 的语法非常直观,甚至有点类似我们熟悉的编程语言中的条件判断。但在实际使用中,它的行为却和直觉并不完全一致。如果不了解其中的细节,很容易写出“看起来正确,但结果却错误”的代码。

其中最常见的一个问题是:if() 的判断依据到底是“计算后的值”,还是“原始值”

这个问题看似简单,却直接决定了你的条件是否能够正确命中,也是很多人第一次使用 if() 时踩坑的根源。接下来,我们通过一个具体的例子来看看这个问题是如何产生的,以及应该如何正确处理。

来看一个简单的例子:

.box {
    --n: 6;
    --f: calc(var(--n) / 2);
    background: if(
        style(--f: 3): red;
        else: green
    );
}

从直觉上看,--n6--f 计算后应该是 3 ,因此条件成立,.box 元素的背景颜色理应是红色(red)。但实际渲染结果却是绿色(green)。

问题的关键在于 if() 中的 style() 并不会基于“计算后的结果”进行判断,而是直接对“原始值”进行字符串匹配。也就是说,浏览器看到的是 calc(var(--n) / 2) 这个表达式本身,而不是它计算后的数值 3 ,因此条件匹配失败。

.box {
    --n: 6;
    --f: calc(var(--n) / 2);
    /* 不匹配,返回 false,因此背景颜色是 green*/
    background: if(
        style(--f: 3): red;
        else: green
    );
}

.box {
    --n: 6;
    --f: calc(var(--n) / 2);
    /* 匹配,返回 true,因此背景颜色是 red */
    background: if(
        style(--f: calc(var(--n) / 2)): red;
        else: green
    );
}

Demo 地址:codepen.io/airen/full/…

正确方式:使用 @property 让值参与计算

如果希望 if() 的判断基于“计算后的结果”,而不是原始字符串,就需要借助 @property 来注册自定义属性。通过这种方式,可以明确告诉浏览器该变量的类型,从而让它在参与比较之前先完成计算与解析。例如:

@property --f {
    syntax: "<number>";
    inherits: false;
    initial-value: 0;
}

.box {
    --n: 6;
    --f: calc(var(--n) / 2);
    background: if(style(--f: 3): red; else: green);
}

在这个例子中,--f 被注册为 <number> 类型,因此浏览器会先对 calc(var(--n)/2) 进行求值,得到数值 3,再参与条件判断。也正因为如此,if() 中的条件能够正确匹配,最终背景颜色会按预期显示为红色。

Demo 地址:codepen.io/airen/full/…

换句话说,一旦自定义属性具备了明确的类型信息,它就不再只是一个“字符串”,而是一个可以参与计算和比较的真正数值。这正是解决该问题的关键所在。

温馨提示:如果你想更深入了解 @property 的用法与原理,推荐继续阅读《CSS 自定义属性: @property》和《Web UI:你需要的是 @property》,可以帮助你更系统地掌握这一特性。

不涉及计算时:无需注册属性

当自定义属性的值只是一个固定值,而不包含 calc() 等计算表达式时,其实不需要使用 @property 进行注册。这种情况下,if() 的判断逻辑非常直接——它只是对值进行字符串层面的精确匹配。例如:

.box {
    --f: error;
    background: if(style(--f: error): red; else: green);
}

.box {
    --v: 0;
    background: if(style(--v: 0): red; else: green);
}

在这个例子中,--f--v 都是明确的静态值,浏览器无需进行额外计算,因此 if() 可以直接完成匹配,并按预期应用对应的样式。

Demo 地址:codepen.io/airen/full/…

也就是说,只要不涉及计算,if() 的行为就是简单可靠的字符串比较,这也是它最直观、最容易理解的一种使用方式。

另一种技巧:使用 = 进行数值比较

除了使用 @property 让属性参与计算之外,还有一种更简洁的方式可以达到相同效果,那就是在 if() 中使用 = 运算符进行匹配。例如:

.box {
    --n: 6;
    --f: calc(var(--n)/2);
    background: if(style(--f = 3): red; else: green);
}

这种写法与使用 : 的行为本质不同。= 会基于“计算后的结果”进行比较,而不是简单的字符串匹配。因此,即使 --f 是通过 calc() 计算得来的,也能正确参与判断。

也正因为如此,这种方式可以在不注册 @property 的情况下,依然得到正确的结果。在很多场景下,它是一种更轻量、更实用的解决方案。

如果你继续深入探索,会发现 if() 还可以与样式查询(style queries)结合,构建更强大的条件判断能力,例如基于范围的比较(>< 等)。这类用法已经超出了基础范畴,属于更进阶的技巧。如果你想进一步了解相关内容,可以查阅《CSS 技巧:样式查询与 if() 函数隐藏技巧》,会有更系统和深入的讲解。

小结

从整体来看,if() 的判断方式可以归纳为两种核心逻辑:一种是使用 : 进行匹配,此时本质上是字符串级别的比较,不会触发计算;另一种是使用 =>< 等,则会基于计算后的结果进行数值比较。

理解这一区别至关重要。它不仅关系到条件是否能够正确命中,也直接决定了你是否需要借助 @property 来让自定义属性参与计算,从而影响最终的渲染结果。

掌握这一点,你就能更从容地在实际项目中使用 if(),避免那些“看起来没问题,但结果却不对”的常见陷阱。

为什么 :is(::before, ::after) 不能工作?

在 CSS 中,:is() 是一个非常实用的函数型伪类,它可以帮助我们简化和合并选择器。很多 Web 开发者都会用它来减少重复代码,让选择器更简洁、更易读。

例如:

button.large,
button.small {
    /* CSS */
}

可以使用 :is() 改写为:

button:is(.large, .small) {
    /* CSS */
}

这样既保持了相同的效果,又让代码更加紧凑。因此,很多人会把 :is() 理解为一种 “合并选择器”的工具

不过,当你对 :is() 越来越熟悉时,可能会产生一个看起来很合理的想法:既然 :is() 可以合并选择器,那是否也可以用来同时选择元素的伪元素?例如:

button:is(::before, ::after) {
    /* CSS */
}

从直觉上看,这似乎是在说:选择 button ::before ::after。但实际上,这段代码不会生效。浏览器会直接忽略它,因为在 CSS 规范中,伪元素是不允许写在 :is() 里的

这就引出了一个非常有意思的问题:为什么 :is(::before, ::after) 不能工作? 要理解这个问题,我们需要先弄清楚一件事: :is() 在 CSS 选择器中到底是如何工作的。 很多开发者对它的理解,其实和浏览器真正的解析方式并不完全一样。

在接下来的内容中,我们会一步一步拆解:

  • :is() 的真实作用是什么

  • 为什么伪元素不能出现在 :is()

  • 为什么 :is(:hover, :focus) 是合法的,而 :is(::before, ::after) 却不是

  • 以及如何正确地编写涉及伪元素的选择器

理解这些规则之后,你不仅能避免一个常见的 CSS 坑,还能更准确地理解 CSS 选择器的工作方式

:is() 的真实作用是什么?

很多人第一次接触 :is() 时,都会把它理解为一种用来合并选择器的语法糖。例如:

button.large,
button.small {
    /* CSS */
}

可以写成:

button:is(.large, .small) {
    /* CSS */
}

从结果上看,两种写法确实是等价的,因此很容易让人产生一个印象: :is() 的作用就是把多个选择器合并成一个。但实际上,这只是表面现象。

在 CSS 选择器中,:is() 的真实作用并不是“展开选择器”,而是为当前元素增加匹配条件。换句话说,:is() 本身是一个伪类(pseudo-class) 。它的行为和 .class#id:hover 这些选择器类似,都是在为元素增加匹配规则。

例如:

button:is(.large, .small) {
    /* CSS */
}

正确的理解方式是:选择所有 既是 button 元素,同时又匹配 :is() 内部条件的元素。也就是:

  • 元素必须是 button

  • 同时具有 .large .small

因此,它并不是先被浏览器转换成:

button.large,
button.small {
    /* CSS */
}

再去匹配元素,而是浏览器直接按照选择器规则进行匹配判断

理解这一点非常重要,因为它会影响我们如何阅读选择器。例如:

:is(.card, .panel) {
    /* CSS */
}

表示:选择任何 匹配 .card .panel 的元素。 而如果写成:

button:is(.card, .panel) {
    /* CSS */
}

含义就变成:选择 既是 button 元素,同时又匹配 .card .panel 的元素。

因此可以总结为一句话: :is() 用来为当前元素增加一组“可选的匹配条件”。 它并不会改变选择器的目标,也不会去“选择”括号里的内容,而只是判断当前元素是否满足其中任意一个条件

正是因为这个机制,才会导致一个容易让人困惑的现象:伪元素(例如 ::before ::after )不能写在 :is() 里面。

为什么伪元素不能出现在 :is() 中?

理解这个问题的关键,仍然是 :is() 的作用。正如前面提到的,:is() 是一个伪类,它只是为当前元素增加匹配条件。也就是说,它是在判断:当前元素是否满足括号中的某个选择器

例如:

button:is(.large, .small) {
    /* CSS */
}

意思是:选择既是 button 元素,同时又匹配 .large.small 的元素。换句话说,:is() 只是用来判断 button 元素是否符合这些条件

现在再来看这个选择器:

button:is(::before, ::after) {
    /* CSS */
}

如果按照同样的逻辑去阅读,它的含义就变成:选择既是 button 元素,同时又匹配 ::before ::after 的元素

问题就在这里,元素不可能是伪元素:

  • button 是一个 真实的 DOM 元素

  • ::before::after伪元素

伪元素并不是独立的节点,而是附属于某个元素生成的内容。因此,一个元素不可能同时既是元素又是伪元素。所以这个选择器的条件永远不可能成立,浏览器也就不会匹配到任何内容。

这也是为什么 :is() 中只允许写普通选择器或伪类,而不能写伪元素。例如下面的写法是完全合法的:

button:is(:hover, :focus, :active){
    /* CSS */
}

它的意思是:选择所有 处于 :hover :focus :active 状态的 button 元素。因为 :hover:focus:active 都是伪类,只是描述元素状态,因此可以作为匹配条件使用。

简单来说,可以记住这样一条规则: :is() 用来给元素增加匹配条件,而不是用来选择伪元素。 同样的限制也适用于另外两个伪类 :not():where()

温馨提示:有关于 :is():not():where() 选择更详细的介绍,请移步阅读《CSS 选择器::where() vs. :is()》!

如何正确地编写涉及伪元素的选择器?

既然 :is() 不能包含伪元素,那么当我们需要同时为多个伪元素编写样式时,应该怎么写?最常见、也是最推荐的方法,就是使用逗号分隔的选择器列表

例如:

button::before,
button::after {
    content: "";
    position: absolute;
}

这种写法虽然稍微长一点,但它清晰、直观,而且完全符合 CSS 规范。

如果多个选择器的主体部分相同,而只有伪元素不同,也通常只能这样写:

.card::before,
.card::after {
    content: "";
    position: absolute;
}

这里实际上是在选择.card::before.card::after。而不是试图把伪元素放进 :is() 中。

不过,我们仍然可以在元素部分使用 :is() ,然后在最后添加伪元素。例如:

:is(button, a)::before {
    content: "";
}

这个选择器的意思是:选择 buttona 元素的 ::before 伪元素。同样的思路也可以用于类选择器:

:is(.card, .panel)::after {
    content: "";
}

表示 .card::after.panel::after

因此,可以记住一个简单的经验法则: :is() 可以用来匹配元素,但伪元素必须写在选择器的最后。 例如:

/* ✅ 正确写法 */
:is(button, a)::before {
    /* CSS */
}

/* ❌ 错误写法 */
button:is(::before, ::after) {
    /* CSS */
}

总结

:is() 是一个非常强大的 CSS 工具,它可以帮助我们减少重复选择器,让代码更简洁。但需要记住两点关键规则:

  • :is() 用于匹配元素条件,而不是选择伪元素

  • 伪元素必须始终写在选择器的最后

理解这一点之后,你不仅可以避免 :is(::before, ::after) 这样的常见错误,还能更深入地理解 CSS 选择器的匹配机制

❌