CSS 原生嵌套 + :has() + :where():2024 后不再需要 Sass 的几个功能

起因

写 CSS 多年装 Sass / Less / postcss-nesting 主要为了嵌套 + 父选择器 +
mixin。但 2023 后浏览器原生支持:

  • CSS Nesting:原生嵌套,与 Sass 兼容语法
  • :has():父选择器(基于子元素状态匹配父元素)
  • :where() / :is():选择器组合 + 控制优先级
  • CSS Layers (@layer):明确分层避免优先级地狱

新项目大多可以不引入 Sass。

1. CSS Nesting

/* 旧:扁平 */
.card { padding: 16px; }
.card .title { font-size: 18px; font-weight: bold; }
.card .title:hover { color: blue; }
.card.active { border: 2px solid blue; }
.card.active .title { color: blue; }

/* 新:嵌套(原生) */
.card {
  padding: 16px;

  & .title {     /* & 显式指代父选择器,规范要求 */
    font-size: 18px;
    font-weight: bold;

    &:hover {
      color: blue;
    }
  }

  &.active {
    border: 2px solid blue;

    & .title {
      color: blue;
    }
  }

  @media (max-width: 600px) {
    padding: 8px;
  }
}

要点:

  • & 是必须的(不像 Sass 可省)
  • 嵌套深度不要超过 3 层(可读性 / 编译 size)
  • @media / @supports 也能嵌套

浏览器支持:Chrome 112+ / Firefox 117+ / Safari 16.5+。
2024 中现代浏览器全支持。

2. :has() —— 父选择器

CSS 历史上一直没法"父元素根据子元素状态变样式"。
:has() 终于解决:

/* 含图片的卡片样式不同 */
.card:has(img) {
  padding: 0;
  background: black;
}

/* 表单包含 invalid input 时整组红边框 */
form:has(input:invalid) {
  border: 1px solid red;
}

/* 卡片包含 .featured 时整张卡黄边 */
.card:has(.featured) {
  border: 2px solid gold;
}

/* "下个兄弟是 h2" 的段落加边距 */
p:has(+ h2) {
  margin-bottom: 24px;
}

/* 复杂:当 ul 包含 li.active 时 */
nav ul:has(li.active) {
  background: var(--accent-bg);
}

/* 没有图片的 article */
article:not(:has(img)) {
  text-align: center;
}

之前要靠 JS 加 class 解决的,现在纯 CSS。

实际例子:自适应高亮表单

<form>
  <label>
    Email
    <input type="email" required>
  </label>
  <label>
    Password
    <input type="password" required minlength="8">
  </label>
</form>
label:has(input:invalid:not(:placeholder-shown)) {
  color: red;
}
label:has(input:valid) {
  color: green;
}

用户输入到无效状态时 label 立刻变红,不需要任何 JS。

3. :where() —— 零优先级组合

:where() 把多个选择器组合,但优先级为 0

/* :is() 优先级 = 最高的子选择器 (这里 #foo = 100) */
:is(.a, #foo) p { color: red; }

/* :where() 优先级 = 0 */
:where(.a, #foo) p { color: red; }

实战:写"基础重置"时希望可以被业务样式轻易覆盖:

/* 用 :where() 把所有标题清零,业务样式不需要 !important 就能改 */
:where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font-weight: 400;
}

/* 业务样式:普通选择器优先级 1 > :where() 的 0 */
.title { font-weight: bold; }   /* 生效 */

不再需要 !important 大战。

:is() 是同样组合但保留优先级,适合短化代码:

/* 旧 */
header h1, footer h1, main h1 { ... }

/* 新 */
:is(header, footer, main) h1 { ... }

4. @layer —— 显式优先级层

/* base 最低 */
@layer base, components, utilities;

@layer base {
  h1 { font-size: 2rem; }
  a { color: blue; }
}

@layer components {
  .btn { padding: 8px 16px; }
  .card { padding: 16px; }
}

@layer utilities {
  .text-center { text-align: center; }
  .hidden { display: none; }
}

后声明的 layer 总是覆盖前面的,无视选择器优先级

意思:

  • @layer base { h1 { ... } } 的 h1(优先级 1)
  • @layer utilities { h1 { ... } } 的同 h1(也是优先级 1)覆盖
    ——因为 utilities 层在后

最强:第三方 CSS 引入时套个低优先级 layer:

@layer reset, vendor, base, components;

@import url('https://cdn.example.com/some-lib.css') layer(vendor);

/* 我的 base 总能覆盖 vendor 的所有 selector,无论它写了多复杂的 .x.y.z 选择器 */

终结"如何覆盖 Bootstrap 的样式"类问题。

5. 用 PostCSS 转旧浏览器

旧浏览器(IE 11、老 Safari)支持差。生产建议加 PostCSS 转译:

npm i -D postcss postcss-nesting postcss-preset-env
// postcss.config.js
export default {
  plugins: [
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'nesting-rules': true,
        'has-pseudo-class': true,
      },
    }),
  ],
}

PostCSS 把 nesting / :has() 等转成等价旧 CSS,老浏览器也能用。

6. 真实重构例子

之前用 Sass 写的组件:

// Card.scss
.card {
  padding: 16px;
  border-radius: 8px;
  background: white;

  &__title {
    font-size: 18px;
    font-weight: bold;
  }

  &__body {
    margin-top: 12px;
  }

  &--featured {
    border: 2px solid gold;

    .card__title {
      color: gold;
    }
  }
}

迁移到原生 CSS:

.card {
  padding: 16px;
  border-radius: 8px;
  background: white;

  & > .title {
    font-size: 18px;
    font-weight: bold;
  }

  & > .body {
    margin-top: 12px;
  }

  &.featured {
    border: 2px solid gold;

    & > .title {
      color: gold;
    }
  }
}

文件几乎一样,少装一个 Sass + sass-loader 依赖。

效果

新项目:

  • 不再装 Sass / Less,CSS pipeline 简化
  • :has() 替代了 5-6 处 JS 操控 class 的逻辑
  • @layer 让多团队 CSS 协作不再撞车
  • DevTools 调试 CSS(不再是编译后的"扁平"代码)

仍要用 Sass 的场景

CSS 原生还没有的:

  • mixin / @function:能写函数复用
  • 运算 / 颜色函数:CSS color-mix() 部分替代
  • partial / @use:CSS @import

如果只用嵌套 + 父选择器 → 原生 CSS 够;要重度逻辑还是 Sass / Stylus。

踩过的坑

  1. & 不能省:写 .card { .title {} } 在原生 CSS 里不工作
    (但 Sass 工作)。必须 & .title

  2. 嵌套里的 type 选择器
    css .card { /* 旧 Sass 可以写 a {} 直接 */ a { color: blue; } /* CSS 也支持,但语义是 ".card a",跟 Sass 一致 */ }
    实际无差。

  3. :has() 性能:浏览器要"反向"匹配,复杂 selector 可能慢。
    单页面大量 :has() 时观察 performance。

  4. @layer 顺序很重要:第一行 @layer a, b, c 定义顺序,
    后续 @layer a 即使写在最后也属于第一层(低优先级)。

  5. PostCSS 转译错 :has():现有 polyfill 不完美(DOM 监听变化
    性能差)。如果一定要老浏览器兼容,避免在频繁变化的元素上用。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。