起因
写 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。
踩过的坑
-
&不能省:写.card { .title {} }在原生 CSS 里不工作
(但 Sass 工作)。必须& .title。 -
嵌套里的 type 选择器:
css .card { /* 旧 Sass 可以写 a {} 直接 */ a { color: blue; } /* CSS 也支持,但语义是 ".card a",跟 Sass 一致 */ }
实际无差。 -
:has()性能:浏览器要"反向"匹配,复杂 selector 可能慢。
单页面大量:has()时观察 performance。 -
@layer顺序很重要:第一行@layer a, b, c定义顺序,
后续@layer a即使写在最后也属于第一层(低优先级)。 -
PostCSS 转译错
:has():现有 polyfill 不完美(DOM 监听变化
性能差)。如果一定要老浏览器兼容,避免在频繁变化的元素上用。
登录后参与评论。