当我看到 CSS 原生支持嵌套时,第一反应是“终于来了”
写过几年前端的人,几乎没有不熟悉 Sass 或 Less 里的嵌套写法。它让样式结构对应 HTML 结构,阅读起来少了很多上下文切换。但嵌套一直不是 CSS 的官方语法,我们不得不为它引入预处理器,在编译阶段将嵌套展开。现在,CSS Nesting 已经成为规范并进入主流浏览器,原始 CSS 终于有了一种原生的嵌套能力。

原生嵌套确实能用,但它真的能让我们向预处理器说再见吗?从工程实践角度看,事情并没有那么简单。嵌套只是预处理器提供的功能之一,而且原生嵌套和 Sass 嵌套在能力上并不完全等价。这篇文章想弄清楚:哪些场景可以放心地拥抱原生 CSS 嵌套,哪些地方会被现实绊住。
原生嵌套语法核心:& 引用的不仅是父选择器
CSS Nesting 中最基本也最容易被误解的是 & 的用法。下面这个例子是原生 CSS 合法写法:
.card {
padding: 16px;
border-radius: 8px;
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card__title {
font-size: 18px;
}
@media (max-width: 600px) {
padding: 8px;
}
}
它等价于浏览器自动将其拆开的样式:
.card {
padding: 16px;
border-radius: 8px;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card .card__title {
font-size: 18px;
}
@media (max-width: 600px) {
.card {
padding: 8px;
}
}
& 在嵌套规则里代表父选择器。你可以将它放在任意位置,也可以省略它——如果省略,嵌套规则会隐式加一个后代选择器。比如上面的 .card__title 在语义上就是 .card .card__title。而 &:hover 确保选择的是 .card:hover 而不是 .card :hover。
这种设计让嵌套可以套在 @media 和 @supports 里,条件规则中的样式会保留父选择器上下文,非常直观。
预处理器与原生嵌套的最大分水岭:字符串拼接
Sass 社区有一种几乎被默认的 BEM 写法:
.button {
&--primary {
background: #0af;
}
&__icon {
margin-right: 8px;
}
}
编译后得到 .button–primary 和 .button__icon。这个模式非常常用,它并不是把 & 当作选择器引用,而是当作字符串占位符,在编译时拼接出新的类名。但原生 CSS 的 & 没有拼接能力,它只是一个真实的父选择器引用。如果你把同样的代码放入原生嵌套,&–primary 会被解析成父选择器后面跟了一个奇怪的自定义标识符,最终被浏览器丢弃或当成无效选择器。
也就是说,原生嵌套能做结构嵌套,不能做词法变换。想用原生 CSS 前缀或后缀生成类名,目前只能显式写完整选择器,比如 .button–primary { … }。这会让“简洁”程度打一些折扣,但换来的是无需编译器、完全可控的原生语义。
能力对比:CSS Nesting 到底缺哪些东西?
| 能力 | CSS Nesting | Sass/Less 嵌套 |
|---|---|---|
| 后代选择器嵌套 | 支持 | 支持 |
| & 父选择器引用 | 支持 | 支持 |
| 类似 &__xxx 的字符串拼接 | 不支持 | 支持 |
| 选择器插值 #{…} | 不支持 | 支持 |
| @extend / @at-root | 不支持 | 支持 |
| Mixin / 宏 / 函数生成样式 | 不支持 | 支持 |
| 条件规则嵌套 | 支持 @media/@supports | 支持 |
| 构建依赖 | 零依赖(现代浏览器) | 必须编译 |
这张表揭示了本质:原生嵌套覆盖的是“结构化组合”,预处理器覆盖的还有“样式生成逻辑”。如果你的项目只在结构上使用嵌套,原生方案足够;如果大量依赖 mixin 和 @extend 来保持样式可维护性,那么预处理器还是不可替代的。
超出表象的坑:嵌套、级联与特异性
只有在真正落地时才容易遇到这些问题。第一个坑是高特异性。很多人觉得嵌套让样式“局部化”,但展开后的规则依然是全局作用域的。比如 .sidebar { .nav { a {} } } 会变成 .sidebar .nav a,三个层级的选择器,特异性不低。后续想覆盖它就更难。
第二个坑是不稳定的隐式后代。前面提到省略 & 会自动变成后代选择器,所以如果你在嵌套里写一个元素选择器,比如 .card { span {} },它会被解析为 .card span,而不是 .card span 之外的东西。这个行为并不直观,尤其从预处理器项目迁移时,容易因为漏写 & 而产生意料之外的后代匹配。
第三个坑是对 :is() / :where() 的特殊处理。CSS 嵌套中 &:is(.a, .b) 的特异性取决于参数里最具体的选择器。如果你在嵌套里用了 :where(),它会消解自身的特异性,导致整个嵌套规则的自毁效果。这在某些极端情况下会让样式被自己人覆盖。
这些坑并不代表原生嵌套不好,而是提醒我们:嵌套不是作用域,只是书写上的折叠。
浏览器兼容性:从“能用”到“生产可用”
| 浏览器 | 首个支持版本 |
|---|---|
| Chrome | 112 |
| Edge | 112 |
| Firefox | 117 |
| Safari | 16.5 |
这意味着如果你的产品要覆盖还在使用 Chrome 100 以下或 Safari 15 左右的用户,直接在生产环境写下原生嵌套是有风险的。比较稳妥的做法是继续在构建链条中加入 PostCSS 的 postcss-nesting 插件,它会把标准语法的嵌套编译为旧浏览器也能理解的扁平 CSS。这样你既能享受原生语法,又不必担心兼容性。
注意,postcss-nesting 只编译符合标准语法的嵌套,不是把 Sass 代码转换成 CSS。将 Sass 嵌套直接搬到标准语法的前提是,别用那些 CSS 不支持的拼接功能。
PostCSS 平滑过渡配置
如果你还在用 PostCSS,可以这样引入原生嵌套支持:
npm install -D postcss postcss-nesting
// postcss.config.js
module.exports = {
plugins: {
'postcss-nesting': {}
}
}
这样配置之后,样式代码里可以直接写 CSS Nesting,构建结果会针对不支持的浏览器输出展开后的普通 CSS。
该不该现在“告别”预处理器?
这个问题没有统一答案,取决于你依赖预处理器的程度。我见过几种典型情况:
- 一个内部管理系统,样式相对简单,只用嵌套、变量,团队正想简化构建链路——这种完全可以切到原生 CSS 嵌套,并搭配 PostCSS 做兜底。
- 一个 UI 组件库,需要向多个业务方输出样式,同时内部高度依赖 Sass 的 mixin、function 和颜色计算——这类项目不会因为原生嵌套而被重写,更现实的是降低嵌套在 Sass 中的占比。
- 一个业务团队已经习惯了 BEM 静态命名,用 Sass 提供的选择器拼接生产类名——如果立刻改成原生嵌套,代码层面会有大规模重构成本,价值有限。
所以如果你问我原生嵌套能不能让我们告别预处理器,我的回答是:在“嵌套”这个具体战线上,它确实有能力完成大部分常规战斗;但整个预处理器并不是只有嵌套这一种武器。
落地建议:从边缘项目开始渐进切换
如果你想让团队逐步走向原生 CSS Nesting,下面几条路径值得参考:
- 找一个非核心的静态页面或活动页,先引入原生嵌套,看看语法效力和代码审查体验。
- 利用 PostCSS 插件做标准语法编译,同时保留 pre-merge 阶段的样式检查。
- 在迁移之前,全局搜索 &__、&– 这类拼接用法,确认它们只出现在可预处理代码里。
- 约定嵌套深度上限,比如建议不超过 3 层,控制特异性和可读性。
- 保持与团队现有风格指南一致,不要把嵌套变成另一种复杂。
另外,如果你正在使用 CSS Modules、Tailwind 这类方案,需要先确认它们的处理层是否支持原生嵌套。Tailwind 本身允许你在样式中使用嵌套,但它的工具函数与原生嵌套结合时,可能需要额外的配置,不能想当然。
结论:原生嵌套是补全,不是替代
CSS Nesting 的出现,让 CSS 这门语言补上了多年缺位的一项表达能力。它有价值,但不是为替代预处理器而生。真正合理的态度是把原生嵌套当作一个现代 CSS 特性来用,该用则用,不必为了“纯 CSS”而刻意放弃预处理器的组合拳;也不必为了“稳定”而拒绝尝试新语法。
也许用不了几年,当浏览器版本全面跟进,当构建工具链越来越轻,我们真的可以更轻松地在更多项目里取消预处理器编译这一层。到那时,那句“告别预处理器的嵌套时代”才算真正落地。而在此之前,它更像是一个信号:CSS 在变好,我们从现在就可以开始适应。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/794/