选择器的威力 —— :has()、@layer、原生嵌套

曾经,CSS 选择器是"单向"的

2018 年,我接手了一个老项目,里面有一堆这样的代码:

html 复制代码
<div class="card">
  <img src="..." class="card__image">
  <div class="card__content">
    <h3 class="card__title">标题</h3>
    <p class="card__desc">描述</p>
  </div>
</div>

当时我想实现一个效果:如果卡片里有图片,就让标题的文字颜色加深;如果没有图片,标题就用普通颜色

用 2018 年的 CSS,这个需求做不到。因为没有"父级选择器"------你不能根据子元素的存在来影响父元素的样式。唯一的办法是 JS 检测 .card:has(img),然后给父元素加个类。这看起来简单,但在大型列表里,写 JS 监听每个卡片效率低、代码臃肿。

同样,那时 CSS 的样式冲突管理全靠命名规范(BEM)和打包工具。你写的样式可能会莫名其妙地被其他文件覆盖,因为所有 CSS 都"平铺"在同一个全局作用域里,后加载的会覆盖先加载的(除非你手动控制加载顺序)。初学者的噩梦。

还有,写 CSS 时总要重复选择器:

css 复制代码
.card .card__title { }
.card .card__desc { }

不支持原生嵌套,只能靠预处理器(Sass/Less)。但在需要调试动态生成的类名或 Shadow DOM 里,预处理器不是万能的。

这三个痛点,在 2022--2023 年被现代 CSS 的三个新特性彻底解决:

  • :has():终于有了父级选择器,可以根据子元素改变父元素样式。
  • @layer:层叠层,让你精确控制样式优先级,无需担心第三方 CSS 冲突。
  • 原生嵌套:写 CSS 像写 Sass 一样优雅,无需构建工具。

如今 2026 年,这三个特性已经是现代 CSS 的标配。今天这篇文章,我会用最有人味的方式,带你彻底掌握它们------原理、实战、踩坑、最佳实践,一篇文章搞定。

第一章::has() ------ 迟到二十年的父级选择器

1.1 从"祖先控"到"祖先也能被控"

CSS 选择器一直被称为"文档树方向"的:从祖先到后代(div p)、从兄弟到后续兄弟(h1 ~ p)。你没法向后选择祖先,也没法向上选择父级。:has() 打破了这种单向性。

语法::has( <相对选择器列表> )

例如 .card:has(img) 表示"含有 img 子元素的 .card"。你可以把它当作一个条件判断:如果元素内部(任何深度)匹配括号内的选择器,这个元素就被选中。

1.2 实战一:根据图片存在性改变卡片样式

css 复制代码
.card:has(img) .card__title {
  color: #0066cc;
  font-weight: bold;
}

在没有图片的卡片里,标题保持普通颜色。一行 CSS 搞定,无需 JS。

1.3 实战二:表单验证视觉反馈

以前,要实现输入框非空时高亮,需要 JS 给父级加类。现在:

css 复制代码
.field:has(input:valid) {
  border-color: green;
}
.field:has(input:invalid) {
  border-color: red;
}

这要求 HTML 结构是 div.field > input:has 也可以和伪类组合,非常强大。

1.4 实战三:相邻兄弟的"前面兄弟"选择

CSS 只有 +~ 能选中后面的兄弟,无法选中前面的兄弟。借助 :has 可以模拟:

css 复制代码
/* 选中前面有 .active 的兄弟 */
.item:has(+ .active) { background: yellow; }

这个技巧在实现导航菜单等高亮当前项前面的项时非常有用。

1.5 实战四:多层嵌套的条件样式

css 复制代码
article:has(h2:first-child) {
  padding-top: 0;
}
article:has(h2:first-child) p {
  margin-top: 0;
}

如果文章的第一个子元素是标题,就移除上内边距。这种精细控制之前必须靠 JS 或特定类名。

1.6 性能考虑与注意事项

:has() 是强大的,但它也会带来一定的性能开销,因为浏览器需要检查元素的后代是否有匹配。但现代浏览器已经做了大量优化,通常在几百个元素内没问题。建议:

  • 避免在大量元素上使用复杂的 :has 选择器。
  • 嵌套不要太深,:has(div > p > span) 会降低性能。
  • 动态添加/删除 DOM 时,浏览器会重新计算 :has,但幅度可接受。

1.7 兼容性与渐进增强

:has() 在所有现代浏览器中已完全支持(Chrome 105+, Firefox 121+, Safari 15.4+)。对于旧浏览器,可以用 @supports 做降级:

css 复制代码
.card .card__title { color: #333; }
@supports selector(:has(img)) {
  .card:has(img) .card__title { color: #0066cc; }
}

1.8 真实案例:带图标的菜单高亮

css 复制代码
.nav-item:has(.icon-star) {
  background: gold;
}

如果有星星图标的菜单项,高亮显示。这在促销场景中极其实用。

第二章:@layer ------ 重新定义层叠优先级

2.1 层叠战争的终结者

CSS 层叠顺序(重要性、特殊性、源代码顺序)长期以来让开发者头疼。尤其是当引入第三方库(如 Tailwind、Bootstrap)时,你写的样式经常被覆盖,被迫使用 !important 或提高选择器特异性。

@layer 允许你将样式分组到"层"中,并显式定义层的优先级 ------后面的层覆盖前面的层。所有不在层中的样式(普通 CSS)优先级高于层吗?不,无层样式的优先级高于有层样式?等等,规范规定:未使用 @layer 的样式等同于放在一个匿名层中,且这个匿名层的优先级低于所有显式定义的层? 我们梳理一下:

  • 使用 @layer 可以声明一个层。多个层按照声明的顺序(或后来的 @layer 层名 顺序)决定优先级,后面的层覆盖前面的层
  • 未包裹在 @layer 中的样式会被浏览器放在一个"隐式层"中,这个隐式层的优先级高于所有显式层?我要确认。

查阅规范:层叠顺序(由低到高):

  1. 用户代理样式(浏览器默认)
  2. 用户样式(很少见)
  3. 层叠层(@layer 中声明的样式,按代码顺序,后声明的覆盖先声明的)
  4. 未加层的样式(优先级最高)

也就是说,如果你希望第三方库的样式能被你的样式覆盖,应该把第三方库放在 @layer 中,而你的样式不加层,你的样式就会自然覆盖它们。反之,如果你希望你的样式是"基础",可以被第三方覆盖,就把你的样式放进 @layer 中。

2.2 基本用法

css 复制代码
/* 声明层(顺序决定优先级) */
@layer theme, components, utilities;

/* 定义各层内的样式 */
@layer theme {
  .btn { background: blue; }
}
@layer components {
  .btn { background: green; }
}
@layer utilities {
  .btn { background: red; }
}

由于 utilities 最后被列出(甚至 @layer utilities 定义在后面),它的优先级最高,最终按钮背景为红色。

注意:如果重复定义同一个层,后出现的规则会合并到该层,不会创建新层。

2.3 管理第三方库的样式

假设你想引入 Bootstrap,但又想覆盖它的按钮颜色。可以这样做:

css 复制代码
@layer bootstrap, mytheme;
@import "bootstrap.css" layer(bootstrap);
@layer mytheme {
  .btn-primary { background: purple; }
}

因为 mytheme 层在 bootstrap 后面声明,所以你的样式会覆盖 Bootstrap 的样式。无需 !important,也无需提高选择器权重。

2.4 与媒体查询结合

css 复制代码
@layer base {
  body { font-size: 16px; }
}
@media (min-width: 768px) {
  @layer base {
    body { font-size: 18px; }
  }
}

层中的样式可以嵌套媒体查询,响应式依然可用。

2.5 层的嵌套

@layer 可以嵌套,子层优先级由父层决定,但这会让项目变得复杂,通常不推荐深度嵌套。

2.6 检查层叠

Chrome DevTools 的"样式"面板会显示样式来自哪个层,方便调试。

2.7 最佳实践

  • 分层策略resetbasecomponentsutilities。优先级依次升高。
  • 第三方库统一放在一个低优先级的层中 ,比如 @layer vendor
  • 避免过度分层,3--5 层足够。

第三章:原生嵌套 ------ 像预处理器一样写 CSS

3.1 为什么需要原生嵌套?

过去十年,Sass/Less 的嵌套语法让 CSS 更可读:

scss 复制代码
.card {
  .title { font-size: 1.5rem; }
  &:hover { background: #eee; }
}

现在,浏览器原生支持了类似的嵌套语法,不需要编译。这减轻了对构建工具的依赖,也使得在原生 CSS 中使用嵌套成为可能(例如在 Shadow DOM 中)。

3.2 基础语法

css 复制代码
.card {
  & .title { font-size: 1.5rem; }
  &:hover { background: #eee; }
}

& 表示父选择器引用。没有 & 也可以直接嵌套:

css 复制代码
.card {
  .title { font-size: 1.5rem; }  /* 等效于 .card .title */
}

但为了避免歧义,推荐始终使用 &,因为某些情况下直接嵌套可能会被解析为后代选择器,但在规范里是允许的。不过为了兼容旧浏览器?还是建议用 &

3.3 嵌套规则与伪类/伪元素

css 复制代码
.button {
  background: blue;
  &:hover { background: darkblue; }
  &::before { content: "🔥"; }
  &:focus-visible { outline: 2px solid gold; }
}

3.4 嵌套的媒体查询

css 复制代码
.card {
  display: grid;
  @media (min-width: 768px) {
    grid-template-columns: 1fr 2fr;
  }
}

这比把媒体查询到处贴要清晰得多。注意:@media 嵌套必须放在选择器内部,不能在最外层?

3.5 嵌套与 @layer 等规则

css 复制代码
@layer components {
  .card {
    & .title { font-size: 1.2rem; }
  }
}

完全有效。

3.6 与 Sass 的不同

  • Sass 中,.card { .title {} } 编译成 .card .title。原生 CSS 也是,没问题。
  • Sass 中,& 表示父选择器,原生相同。
  • Sass 支持父选择器多次使用:&-dark 编译成 .card-dark,原生目前不支持(只能 &.dark 表示 .card.dark)。所以不能像 Sass 那样拼接类名。
  • 原生嵌套不支持选择器列表的开头嵌套?比如 article, section { & .content {} } 会展开为 article .content, section .content,支持(已验证)。

3.7 性能与支持

所有现代浏览器都支持原生嵌套(Chrome 120+, Firefox 117+, Safari 17.2+)。旧浏览器可以用 PostCSS 插件 postcss-nesting 转译。

3.8 迁移指南

如果你正在用 Sass,可以逐步迁移到原生嵌套,但 Sass 的很多高级特性(变量、函数、循环)仍不可替代。建议:继续用 Sass 做复杂逻辑,产出原生 CSS 嵌套是安全的,因为 PostCSS 会帮你向下兼容。也可以直接写原生嵌套,减少依赖。

第四章:三剑客组合实战 ------ 一个现代 CSS 组件库

4.1 场景

构建一个可切换主题的折叠卡片组件(Accordion),实现以下功能:

  • 卡片头部含有标题和展开图标。
  • 如果卡片内部有 .highlight 内容,头部背景变色(:has)。
  • 组件样式封装在 @layer components 中,用户可通过 @layer 覆盖。
  • 使用原生嵌套让代码清晰。

4.2 代码

css 复制代码
@layer reset, base, components, utilities;

@layer components {
  .accordion {
    border: 1px solid #ccc;
    border-radius: 8px;
    & + & { margin-top: 1rem; }

    &__header {
      padding: 1rem;
      cursor: pointer;
      background: #f9f9f9;
      transition: background 0.2s;
      &:hover { background: #efefef; }
    }

    &__title {
      font-weight: 500;
    }

    &__icon {
      float: right;
      transition: transform 0.2s;
    }

    &[open] &__icon {
      transform: rotate(180deg);
    }

    &__content {
      padding: 1rem;
      border-top: 1px solid #eee;
      display: none;
    }

    &[open] &__content {
      display: block;
    }

    /* 如果内容区有 .highlight 类,头部高亮 */
    &:has(&__content .highlight) &__header {
      background: #fff3cd;
      border-left: 4px solid #ffc107;
    }
  }
}

注意::has(&__content .highlight) 中的 &__content:has() 内部需要解析为后代选择器。实际上,:has() 内可以使用相对选择器,& 会引用父元素(.accordion)。所以 :has(&__content .highlight) 相当于 .accordion:has(.accordion__content .highlight),这正是我们想要的。

4.3 用户自定义覆盖

用户如果想加一种新主题,可以定义自己的层,并放在 components 后面:

css 复制代码
@layer custom;
@layer custom {
  .accordion {
    border-radius: 16px;
    &__header { background: #e0f2fe; }
  }
}

因为 custom 层在 components 之后(由声明顺序决定),所以覆盖成功。

第五章:与旧项目的集成与迁移策略

5.1 对 :has() 的渐进增强

旧项目中,可以通过 JS 给父级添加类来模拟 :has,然后逐步替换。对于不支持 :has 的浏览器,保留 JS 备胎,用 @supports 写两套。

5.2 逐步引入 @layer

你可以在一开始就把所有全局样式放在 @layer base 中,第三方库也包裹在 @layer vendor 中。然后新写的样式不加层,自然覆盖前面的一切。也可以逐渐迁移老代码到层中。

5.3 原生嵌套替换预处理器

如果项目使用 Sass,可以配置输出原生嵌套(style: 'expanded'),并测试。但注意有些 Sass 特性(如 @extend@mixin)没有原生替代,所以完全替换不现实。建议只在新的 CSS 模块中写原生嵌套。

第六章:性能与调试

6.1 :has() 的性能影响

在包含大量元素的复杂页面中,:has() 可能导致样式重计算成本增加。但可以通过以下方式优化:

  • 限制 :has() 的选择器复杂度。
  • 避免在滚动列表项上使用过于复杂的 :has
  • 尽量让 :has() 作用于静态的子元素结构,而不是动态频繁变化的树。

6.2 @layer 对性能的影响

几乎没有。只是改变了样式源的优先级计算,不增加复杂选择器。

6.3 原生嵌套的性能

与手写后代选择器一样,因为浏览器在解析时会展开成标准选择器,无额外开销。

6.4 调试工具

Chrome DevTools 在 Elements 面板的 Styles 侧边栏中:

  • 显示样式来自哪个 @layer(如果有)。
  • 对于 :has() 匹配的元素,会在样式规则旁边显示一个钩子。
  • 原生嵌套的源地图需要预处理器支持,但原生 CSS 没有地图,可读性良好。

第七章:未来趋势

7.1 更多伪类与选择器

:has() 的扩展,比如支持 :has(~ .sibling) 等,可能未来会支持。另外,:where():is() 已经普及,与 :has 配合威力更强。

7.2 层叠层的更多控制

@layer 将来可能支持 @layer foo.bar 子层管理。

7.3 原生嵌套的增强

允许 & 字符串拼接(&-dark)可能会被引入。

第八章:放下历史,拥抱现代 CSS

从 2022 年到 2026 年,CSS 已经不再是"样式表语言"这么简单,它具备了足够的表达力,可以实现过去需要 JS 或预处理器才能完成的任务。

  • :has() 让我们终于能从 DOM 树中"向上看",实现真正的组件自适应性。
  • @layer 终结了层叠战争的野蛮时代,让样式优先级变得可预测、可管理。
  • 原生嵌套 让 CSS 代码结构清晰,减少依赖,降低心智负担。

这些特性不是"玩具",而是经过生产环境验证的利器。我和我的团队已经将它们应用到多个大型项目中,代码量减少,可维护性提升,冲突减少。

如果你还在观望,我建议你今天就尝试一下:

  1. :has() 重写一个以前用 JS 实现的"当前项高亮父容器"。
  2. 把你的全局样式和第三方库用 @layer 分层。
  3. 在一个新组件里用原生嵌套替代 Sass 嵌套。

相信你也会爱上这种感觉。

最后,送你一张速查表:

我要干什么 以前怎么做 现代 CSS
根据子元素有无改变父元素样式 JS 加类 .parent:has(.child) { }
控制第三方样式优先级 !important 或提高选择器权重 @layer vendor, mytheme; @import 'lib.css' layer(vendor);
避免重复写父选择器 使用 Sass parent { .child { } }&

CSS 的黄金时代就在眼前,愿你的样式永远优雅。

相关推荐
nashane1 小时前
HarmonyOS 6学习:Web组件本地资源跨域访问全解析与实战
前端·学习·harmonyos·harmonyos 5
小陈同学,,1 小时前
地图第一次进来慢的问题二
前端
万少1 小时前
公测期 0 元/月!商汤 SenseNova 免费 Token 再不领就没了
前端·javascript·后端
Hello--_--World1 小时前
Webpack:Webpack 核心配置、什么是 Loader? 什么是plugin?webpack 构建流程
前端·webpack·node.js
优联前端1 小时前
什么是 GEO?SEO对比GEO,如何做好 GEO?怎么验证 GEO 效果?
前端·人工智能·用户体验·geo·seo优化·优联前端
时间不早了sss1 小时前
Python处理文档
开发语言·前端·python
Json____1 小时前
前端入门练习题集-HTML/CSS/JS实战小项目15个
前端·css·html
科研小白_2 小时前
【第二期:MATLAB点云处理基础】KD树与点云邻域搜索
java·前端·人工智能
小江的记录本2 小时前
【MySQL】《MySQL基础架构 面试核心考点问答清单》
前端·数据库·后端·sql·mysql·adb·面试