曾经,前端项目里没有 Sass 或 Less 简直寸步难行,光是父子选择器的重复书写就足够让人崩溃。但就在去年,所有浏览器引擎都实现了原生 CSS 嵌套(CSS Nesting),这意味着我们终于可以在纯 CSS 文件中写类似 SCSS 的嵌套语法,而完全不需要构建工具。这篇文章不会停留在语法介绍,而是从真实业务组件出发,把原生嵌套的优势和局限性彻底讲明白。
一、原生嵌套解决了什么真实痛点
拿一个常见的评论列表举个例子。在传统 CSS 里,为了避免类名冲突,我们往往会这样写:
.comment { /* ... */ }
.comment__header { /* ... */ }
.comment__header .comment__avatar { /* ... */ }
.comment__body { /* ... */ }
.comment__body .comment__text { /* ... */ }
.comment__footer { /* ... */ }
哪怕用了 BEM 命名,重复的 .comment 前缀依然占满屏幕。而套上 SCSS 之后,代码马上清爽:
.comment {
&__header {
& .avatar { }
}
&__body { }
&__footer { }
}
原生 CSS 嵌套的目标正是这种表现力。现在浏览器支持它了,我们可以在 .css 文件里直接写:
.comment {
& .comment__header {
& .comment__avatar { }
}
}
不过,真实的原生嵌套语法跟 SCSS 有一个根本区别:它要求嵌套的选择器必须以符号开头,不能直接写一个裸的类名。也就是说 & .child 是合法的,但 .child 自身开头则不行——这是因为浏览器需要明确区分“规则”和“属性”。这个限制也意味着 BEM 那种 &__child 的拼接写法在原生里是可行的,因为 &__child 以 & 开头。
二、上手第一例:一个注册表单
下面用一个注册表单来演示原生嵌套的完整结构。表单包含输入框、标签、错误信息和一个提交按钮。
.form {
display: flex;
flex-direction: column;
gap: 16px;
& .form-group {
display: flex;
flex-direction: column;
& label {
font-size: 14px;
margin-bottom: 4px;
color: #333;
}
& input {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 6px;
&:focus {
border-color: #4a90d9;
outline: none;
}
&:user-invalid {
border-color: #e74c3c;
}
}
& .error-message {
color: #e74c3c;
font-size: 12px;
margin-top: 4px;
display: none;
}
&:has(input:user-invalid) .error-message {
display: block;
}
}
& .form-submit {
padding: 10px 20px;
background: #4a90d9;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
&:hover {
background: #357abd;
}
&:active {
background: #2a5f9e;
}
}
}
注意几个关键点:
- 每个嵌套的选择器前都加了
&(或者以伪类、属性选择器等符号开头)。这是目前原生嵌套的硬性要求,不能省略。 - 伪类如
:focus、:hover可以直接写,因为它们以冒号开头,浏览器能识别。 - 用了
:user-invalid这个相对较新的伪类,它在用户交互后才触发校验显示,比:invalid体验更好。 :has()与嵌套配合可以写出非常内聚的兄弟选择器逻辑,再也不用把错误状态的处理扔到文件底部单独写了。
如果你把这个表单放到任何支持嵌套的浏览器里,它都能完美工作,不需要任何编译步骤。对于小项目或者简单的页面,完全可以用一个 .css 文件就搞定所有样式。
三、与 SCSS 的细微差异——这些坑需要提前知道
很多开发者一听说原生嵌套就以为可以直接把 .scss 文件改个后缀名,但实际上有几点必须调整。
3.1 必须使用 & 作为前缀
SCSS 可以写 .header { .logo { } },但原生 CSS 不允许。你必须写成 .header { & .logo { } }。原因前面提过了,浏览器解析器需要判断后面是属性还是选择器,以符号开头成为一个明确的标记。
3.2 不支持字符串拼接生成类名
SCSS 的 &__element 可以动态拼接成 .block__element,原生嵌套也支持 &__element,因为 & 会被替换为父选择器的文本。所以 BEM 中的元素拼接在原生中是直接可用的。但 &--modifier 同样有效。但是 &.active 这种表示同一元素同时具有两个类的写法也一样没问题。
3.3 媒体查询与容器查询的嵌套
原生嵌套允许将 @media 或 @container 写在规则内部,这点和 SCSS 类似:
.card {
display: grid;
grid-template-columns: 1fr 1fr;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
3.4 层叠层的配合
嵌套与 @layer 可以一起使用,实现更细粒度的优先级控制。
四、案例二:一个多层嵌套的导航菜单
下拉菜单是嵌套选择器大显身手的场景。我们用原生嵌套写一个带有二级下拉的导航,并利用 :hover 和 :focus-within 展开菜单。
.nav {
list-style: none;
display: flex;
gap: 24px;
padding: 0;
& .nav-item {
position: relative;
& .nav-link {
text-decoration: none;
color: #333;
padding: 8px 0;
display: block;
&:hover {
color: #4a90d9;
}
}
& .subnav {
position: absolute;
top: 100%;
left: 0;
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 8px;
padding: 8px 0;
list-style: none;
min-width: 160px;
opacity: 0;
visibility: hidden;
transform: translateY(4px);
transition: all 0.2s;
& .subnav-item {
& .subnav-link {
display: block;
padding: 8px 16px;
color: #555;
text-decoration: none;
&:hover {
background: #f5f5f5;
color: #333;
}
}
}
}
&:hover .subnav,
&:focus-within .subnav {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
}
这段样式读起来就像 DOM 结构的镜像。原生嵌套带来的最大好处就是这种结构性:从哪里到哪里,父子关系一目了然,不需要在脑中展开一大段平铺的选择器。
五、嵌套与 @layer 的联合作战
有些旧代码使用了 !important 或者优先级过高的选择器,重构时可以用 @layer 配合嵌套把不同模块的优先级理顺。比如把基础样式、组件样式、工具类分到不同层:
@layer base, components, utilities;
@layer base {
body {
font-family: system-ui;
& a {
color: inherit;
}
}
}
@layer components {
.card {
border: 1px solid #eee;
& .card-title {
font-weight: bold;
}
}
}
@layer utilities {
.mt-4 {
margin-top: 16px;
}
}
层叠层的顺序保证了即使 .card-title 的权重很低,也不会被后面层的样式意外覆盖,嵌套让每层内部的逻辑自成体系。
六、兼容性与构建工具的关系
截至所有主流浏览器的最新版本,原生 CSS 嵌套已无需开启实验性标志即可使用。如果你的项目仍需兼容少量旧版浏览器,可以利用 PostCSS 的 postcss-nesting 插件将嵌套展开为普通选择器。这样开发时写嵌套语法,构建后兼容所有浏览器,并且不会像 SCSS 那样引入另一种语言依赖。
另外,即使你还在用 SCSS,也不妨碍你逐步使用原生嵌套。两者可以共存,SCSS 解析器遇到 & 前缀的选择器也能正常处理,未来迁移时只需要去掉 SCSS 语法糖,保留嵌套部分即可。
七、总结
原生 CSS 嵌套不是要杀死 SCSS,它真正改变的是那些不想引入预处理器的小型项目、组件库的样式发布方式,以及在不便配置构建工具的场合下编写可维护样式的能力。看完这三个案例,不妨在下一个个人项目或者组件定义里尝试全用标准 CSS 来写,你会发现手感和 SCSS 已经非常接近,而产出的文件却是一个可以被任何浏览器直接消费的 .css 文件。这才是 Web 标准进化的方向。

