CSS 原生嵌套实战:把 SCSS 迁移到现代浏览器的完整指南

2026-06-29 0 628

曾经,前端项目里没有 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 标准进化的方向。

CSS 原生嵌套实战:把 SCSS 迁移到现代浏览器的完整指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css CSS 原生嵌套实战:把 SCSS 迁移到现代浏览器的完整指南 https://www.taomawang.com/web/css/2293.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务