如何使用原生 CSS Mixin 替代 Sass
了解如何使用 @mixin 和 @apply 将 Sass 混入迁移到原生 CSS、处理参数,并判断哪些功能仍需 Sass。
原生 CSS mixin 是一个具名的声明块。你用 @mixin --name 定义它,再用 @apply --name 将其插入样式规则中。因此,它相当于浏览器端版本的 Sass @mixin 与 @include。
如果你的样式表已经在使用自定义属性(custom properties)和原生嵌套,那么 _mixins.scss partial 文件中那几个共享 mixin,可能就是你仍在运行 Sass 的唯一理由。本文将一个真实的 mixin 移植为原生 CSS,把两个版本并排对比,并列出哪些类型的 mixin 无法迁移。
核心要点
- 原生 CSS mixin 使用
@mixin --name定义代码块,使用@apply --name调用它。名称必须是双连字符标识符(dashed ident),@apply承担的正是 Sass 中@include的职责。 - Sass 在编译时会把 mixin 的声明复制到每一条引入它的规则中。原生 mixin 则由浏览器展开,因此最终交付的样式表中只包含一份定义。
- 据 MDN 报告,目前没有任何浏览器默认支持 CSS mixin,因此它尚未进入 Baseline,生产环境仍应选择 Sass。
- 依赖 Sass map、
@each或@for循环、@if/@else分支或构建时数学运算的 mixin,没有对应的原生实现。
你现有的 Sass Mixin
cover mixin 会将元素固定到其已定位祖先元素的四条边上。它几乎是最简单的 mixin 了,因此很适合作为测试用例。下面的代码中它被使用了两次:一次用于背景遮罩,一次用于伪元素覆盖层。(如需进一步了解 Sass 方面的背景知识,请参阅这篇 SCSS mixin 指南。)
@mixin cover {
position: absolute;
inset: 0;
}
.dialog-backdrop {
@include cover;
background: rgb(0 0 0 / 0.6);
}
.media-card.is-loading::after {
@include cover;
content: "";
background: rgb(255 255 255 / 0.7);
}
mixin 定义本身不会生成任何 CSS。每一次 @include 都会把声明复制到调用它的规则中:
.dialog-backdrop {
position: absolute;
inset: 0;
background: rgb(0 0 0 / 0.6);
}
.media-card.is-loading::after {
position: absolute;
inset: 0;
content: "";
background: rgb(255 255 255 / 0.7);
}
引入该 mixin 的规则越多,输出中这两条声明的副本就越多。
用原生 CSS Mixin 重写:并排对比
将一个只包含声明的 Sass mixin 移植为原生 CSS,只需三个机械化的步骤:在名称前加上两个连字符,将 $ 参数改为 -- 参数,并用 @apply 替换 @include。
Sass
@mixin cover {
position: absolute;
inset: 0;
}
.dialog-backdrop {
@include cover;
background: rgb(0 0 0 / 0.6);
}
原生 CSS
@mixin --cover {
position: absolute;
inset: 0;
}
.dialog-backdrop {
@apply --cover;
background: rgb(0 0 0 / 0.6);
}
@apply 取代了 @include,而 mixin 名称必须是双连字符标识符。由于展开工作由浏览器完成,你交付的样式表中只包含一份 --cover 定义。mixin 主体只是普通声明,无需 @result 包裹,因为 CSSWG 已决议从 mixin 中移除 @result。草案本身也提醒,mixin 的成熟度远不及自定义函数,因此语法仍可能发生变化。
CSS Mixin 的浏览器支持情况
原生 CSS mixin 目前还无法用于生产环境。MDN 关于自定义函数与 mixin 的指南指出,没有任何浏览器默认支持 mixin,这意味着该特性尚未进入 Baseline。Chromium 走在其他引擎前面:它已提交了 CSS mixin 的 Intent to Prototype,但这项工作仍处于实验阶段。
原生 CSS mixin 也无法作为渐进增强来使用。CSS 错误处理机制会丢弃解析器无法理解的结构,因此不支持该特性的浏览器会跳过 @apply,而一个仅从 mixin 获得 position 和 inset 的元素,最终两者都得不到。生产环境请保留 Sass 版本,并在 Sass 编译器不会处理的纯 .css 文件中试用原生 mixin。
移植 Mixin 参数
传递简单值的 Sass mixin 参数,可以顺利移植到原生 CSS mixin。而在构建时进行计算、分支或循环的 mixin 则不行。下面是带有 inset 参数的 cover:
// Sass
@mixin cover($inset: 0) {
position: absolute;
inset: $inset;
}
.frame::before { @include cover(0.5rem); content: ""; }
/* Native */
@mixin --cover(--inset) {
position: absolute;
inset: var(--inset);
}
.frame::before { @apply --cover(0.5rem); content: ""; }
每个参数都会成为 mixin 主体内部的私有属性,通过 var() 读取,页面上的其他样式无法访问它。var() 回退值在 mixin 主体中的用法与在任何其他声明中完全相同。CSS Custom Functions and Mixins 草案还定义了专门的语法,用于为参数指定类型和默认值。请查阅草案了解当前的写法,不要想当然地认为回退值与默认值的行为完全一致。2025 年 5 月 15 日发布的首个公开工作草案(First Public Working Draft)要求即使是无参数的 mixin 也必须带括号,而之后的草案允许省略括号。早期的原型构建版本要求写成 @mixin --a() 和 @apply --a();,因此你会同时见到这两种写法。
以下 Sass mixin 特性没有对应的原生 CSS 实现:
@each和@for循环@if/@else分支以及@errorsass:map模块中的 map 及map.get- 构建时数学运算,例如
math.div或单位换算
断点 mixin 清楚地展示了这条分界线:
@use "sass:map";
$breakpoints: (sm: 40rem, md: 64rem);
@mixin bp($name) {
@if not map.has-key($breakpoints, $name) {
@error "Unknown breakpoint: #{$name}";
}
@media (min-width: map.get($breakpoints, $name)) {
@content;
}
}
规范将 @contents 定义为 Sass @content 的对应物,因此理论上原生 mixin 可以接收调用方传入的代码块,并用 @media 将其包裹。无法移植的部分是 map 查找、@error 分支,以及媒体查询中的可变宽度。
原生 CSS Mixin 能带来什么好处?
与 Sass mixin 相比,原生 CSS mixin 有两大优势:无需构建步骤,以及参数在浏览器中而非编译时解析。首先,浏览器直接读取 @mixin 和 @apply,因此你编写的 CSS 就是你交付的 CSS。
第二个优势则更为微妙。Sass 同样可以输出 var() 引用,所以 Sass mixin 也能生成在运行时响应自定义属性的声明。但 Sass 做不到的是在编译后更改参数:cover(0.5rem) 会在构建期间一次性变成 inset: 0.5rem。原生 mixin 在浏览器中解析,而且规范中包含了对”值取决于 mixin 所应用元素”的参数的处理逻辑。不过,这一行为只存在于规范中,尚未在任何已发布的引擎中实现。自定义函数是同一模块中的姊妹特性,使用 @function 定义,它返回单个值,而 mixin 返回的是一个声明块。
什么情况下应继续使用 Sass?
cover、居中和 visually-hidden 这类只包含声明的 mixin 可以顺利移植。而生成选择器、遍历列表或在构建时计算值的 mixin 则不行。
| Mixin | 能否顺利移植? | 需要改动的地方 | 无法移植的原因 |
|---|---|---|---|
| Cover / inset | 能 | -- 名称、@apply、var(--inset) | 不适用 |
| Visually-hidden | 能 | -- 名称、@apply | 不适用 |
| 居中(Centering) | 能 | -- 名称、@apply | 不适用 |
| 断点(Breakpoints) | 不能 | 理论上可通过 @contents 包裹代码块 | map 查找、@error、可变的媒体查询宽度 |
对于生成工具类的循环、由 map 驱动的设计令牌(token)以及断点辅助工具,请继续使用 Sass @mixin 和 @include。在浏览器默认支持 mixin 之前,生产环境中的所有代码也都应继续使用 Sass。
总结
将 cover 这类只包含声明的 mixin 切换为原生语法既快速又基本是机械化操作。何时切换则取决于浏览器支持,而这种支持目前尚不存在。一个切实可行的下一步是:将你的 mixin 分为两组,一组是只包含声明的 mixin,另一组是依赖 Sass 逻辑的 mixin。在单独的 .css 文件中重写第一组,并在实验性的 Chromium 构建版本中进行测试。在 MDN 标注默认支持之前,请保留 Sass 版本。
常见问题
如何在 Chrome 中测试原生 CSS mixin?
Chromium 的 mixin 原型隐藏在 CSSMixins 功能标志之后。如需试用,请通过命令行启动 Chrome Canary,并添加 --enable-features=CSSMixins 参数。该实现尚不完整,并会随着 CSSWG 解决规范问题而变化,因此部分示例可能会失败,或表现出与草案不同的行为。请仅将其用于实验和规范反馈,切勿用于生产环境样式。
为什么不能在媒体查询中用 var() 替代 Sass 断点变量?
var() 函数只能在属性值中使用,而媒体查询条件并不是属性值。自定义属性是通过层叠针对每个元素解析的,而媒体查询是根据视口或设备进行求值的,与任何元素无关。因此,像 min-width: var(--md) 这样的条件是无效的,存储在变量中的断点宽度无法从 Sass 迁移到原生媒体查询中。
原生 CSS mixin 能否包含嵌套选择器或媒体查询?
按照规范是可以的。CSS Custom Functions and Mixins 草案允许在 mixin 主体中使用嵌套样式规则(例如 & ::after 代码块)以及 @media 等条件规则,这与 Sass mixin 能够生成的内容十分相似。但目前没有浏览器默认支持 mixin,因此这描述的是规范预期的行为,而非任何稳定版引擎中能够实际运行的功能。
postcss-mixins 与原生 CSS mixin 是一回事吗?
不是。postcss-mixins 插件使用 @define-mixin 定义 mixin,使用 @mixin 调用它,并使用在构建时展开的美元符号参数。原生 CSS 则使用 @mixin 定义 mixin,使用 @apply 调用它,并由浏览器负责展开。@mixin 关键字在这两套体系中的含义恰好相反,因此从 postcss-mixins 迁移到原生语法时,需要同时重写定义和调用处。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k