主题
CSS 新特性
概览
近年来 CSS 经历了爆发式进化,许多曾经必须依赖 JS 或预处理器的能力被原生支持。以下按 实用优先级 梳理最重要的新特性:
2020 ─────────────────── 2022 ─────────────────── 2024 ───────── 2025+
| | | |
gap (Flex) :has() @starting-style anchor()
aspect-ratio @container @view-transition if()
:is() / :where() @layer light-dark() @function
content-visibility color-mix() field-sizing Mixins
Subgrid interpolate-size
Nesting @property@layer 级联层
问题:CSS 优先级失控
传统 CSS 中,特异性(Specificity)决定哪条规则胜出。当项目引入第三方库(如组件库、CSS Reset),优先级冲突频发:
开发者写的样式 特异性: 0-1-0 (.btn)
第三方组件库样式 特异性: 0-2-0 (.ui-kit .btn)
CSS Reset 特异性: 0-0-1 (button)
→ 第三方库总是赢,开发者被迫用 !important 或提高选择器复杂度@layer 的解决方案
@layer 允许你显式声明样式层的优先级顺序,与特异性无关:
css
/* 声明层顺序:越靠后优先级越高 */
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
color: #333;
}
a { color: #4a90d9; }
}
@layer components {
.card {
padding: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
}
@layer utilities {
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
.hidden { display: none; }
}核心规则
层优先级(低 → 高):
reset → base → components → utilities → 【未分层的样式】
关键点:
1. 层的顺序由 @layer 声明的顺序决定(与选择器特异性无关)
2. 未分配到任何 layer 的样式优先级最高
3. 同一层内仍遵循正常的特异性规则
4. !important 在层中的行为会反转:低优先级层的 !important 反而赢!important 在层中的反转行为
css
@layer base, override;
@layer base {
.text { color: red !important; } /* 这条会赢! */
}
@layer override {
.text { color: blue !important; }
}
/*
正常样式:后声明的层优先级高(override > base)
!important:反转!先声明的层优先级高(base > override)
设计意图:低优先级层的 !important 是"强制保底",不应被高层覆盖
*/实际应用:管理第三方样式
css
/* 将第三方库的样式放入低优先级层 */
@layer lib, app;
@layer lib {
@import url('antd.css');
}
@layer app {
/* 你的样式总是能覆盖 antd,无需提高特异性 */
.ant-btn {
border-radius: 8px;
}
}:has() 关系选择器
被称为"CSS 的 if 语句"——根据子元素或后续兄弟元素的状态选中父元素。
css
/* 选中"包含 img 的 card" */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
/* 选中"包含 img 但不包含 .badge 的 card" */
.card:has(img):not(:has(.badge)) {
padding-top: 0;
}
/* 选中"没有任何子元素的容器"(空状态) */
.container:has(> :first-child:last-child) {
display: flex;
justify-content: center;
}实用场景
表单验证反馈:
css
/* 当输入框处于 :invalid 状态时,标签变红 */
label:has(+ input:invalid) {
color: #e74c3c;
}
/* 当表单内有任何 invalid 字段时,禁用提交按钮的样式 */
form:has(:invalid) .submit-btn {
opacity: 0.5;
pointer-events: none;
}全局状态响应:
css
/* 当页面有打开的 dialog 时,给 body 加模糊 */
body:has(dialog[open]) > main {
filter: blur(4px);
pointer-events: none;
}
/* 暗色模式切换(无需 JS) */
:root:has(#dark-mode:checked) {
--bg: #1a1a2e;
--text: #eee;
}条件布局:
css
/* 只有当 sidebar 存在时,main 才使用双列布局 */
.layout:has(.sidebar) {
display: grid;
grid-template-columns: 240px 1fr;
}
/* sidebar 不存在时,单列 */
.layout:not(:has(.sidebar)) {
display: block;
}:has() vs JavaScript
之前(需要 JS): 现在(纯 CSS):
───────────────── ─────────────
1. JS 监听 DOM 变化 :has() 自动响应
2. 添加/移除 class 无需中间状态
3. 重新计算样式 浏览器原生优化
优势:减少 JS 逻辑、无重排/重绘延迟、声明式思维
限制:不能跨文档、不能向上穿越 Shadow DOM@container 容器查询
问题:媒体查询的局限
媒体查询基于视口宽度,但组件往往在不同容器中复用:
┌──────────────── 视口 1200px ──────────────────┐
│ │
│ ┌── sidebar 300px ──┐ ┌── main 900px ──────┐│
│ │ │ │ ││
│ │ .card → 窄布局 │ │ .card → 宽布局 ││
│ │ │ │ ││
│ └────────────────────┘ └─────────────────────┘│
└─────────────────────────────────────────────────┘
媒体查询看到的是 1200px,但 sidebar 中的 card 只有 300px 可用空间容器查询的解决方案
css
/* 1. 声明容器 */
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
.main {
container-type: inline-size;
container-name: main;
}
/* 2. 基于容器宽度查询 */
.card {
display: grid;
gap: 16px;
}
@container (min-width: 500px) {
.card {
grid-template-columns: 200px 1fr;
}
}
@container (max-width: 499px) {
.card {
grid-template-columns: 1fr;
}
}
/* 指定容器名称 */
@container sidebar (max-width: 350px) {
.card { font-size: 14px; }
}container-type 详解
css
.box {
/* inline-size:只监控行内尺寸(通常是宽度),最常用 */
container-type: inline-size;
/* size:同时监控宽度和高度 */
container-type: size;
/* normal(默认):不作为查询容器,但可用于 style() 查询 */
container-type: normal;
}
/* 简写 */
.box {
container: sidebar / inline-size;
/* name / type */
}容器查询单位
css
/* 容器查询引入了新的相对单位 */
.card-title {
/* cqw: 容器宽度的 1% */
font-size: clamp(16px, 4cqw, 24px);
}
/*
cqw — 容器宽度的 1%
cqh — 容器高度的 1%
cqi — 容器行内尺寸的 1%(LTR 下 = cqw)
cqb — 容器块尺寸的 1%(LTR 下 = cqh)
cqmin — cqi 和 cqb 的较小值
cqmax — cqi 和 cqb 的较大值
*/Style Queries(样式查询)
除了尺寸,还能查询容器的 CSS 自定义属性值:
css
.card {
--variant: default;
}
.card.featured {
--variant: featured;
}
/* 根据容器的自定义属性值应用样式 */
@container style(--variant: featured) {
.card-title {
font-size: 24px;
color: #4a90d9;
}
.card-badge {
display: block;
}
}CSS Nesting 原生嵌套
CSS 原生支持嵌套语法,不再需要预处理器的嵌套功能:
css
/* 原生 CSS Nesting(所有现代浏览器已支持) */
.card {
padding: 16px;
background: #fff;
border-radius: 8px;
.title {
font-size: 18px;
font-weight: bold;
}
.description {
color: #666;
margin-top: 8px;
}
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.featured {
border: 2px solid #4a90d9;
}
@media (max-width: 768px) {
padding: 12px;
.title {
font-size: 16px;
}
}
}与 Sass 嵌套的差异
css
/* Sass:嵌套选择器可以直接写 */
.card {
h2 { color: red; } /* ✅ Sass OK */
}
/* 原生 CSS Nesting(旧规范):类型选择器前需要 & */
.card {
& h2 { color: red; } /* ✅ 需要 & */
h2 { color: red; } /* ✅ 2024年后的浏览器也已支持 */
}
/* 两者都支持的写法 */
.card {
& .title { } /* ✅ 嵌套 class */
&:hover { } /* ✅ 伪类 */
&::before { } /* ✅ 伪元素 */
&.active { } /* ✅ 复合选择器 */
& + & { } /* ✅ 兄弟选择器 */
@media (width < 768px) { } /* ✅ 嵌套 @规则 */
}color-mix() 颜色混合
在 CSS 中原生混合两种颜色,替代预处理器的 lighten() / darken():
css
.btn {
--brand: #4a90d9;
background: var(--brand);
/* 悬停时:品牌色与黑色混合(加深 20%) */
&:hover {
background: color-mix(in srgb, var(--brand) 80%, black);
}
/* 禁用时:品牌色与白色混合(变浅 50%) */
&:disabled {
background: color-mix(in srgb, var(--brand) 50%, white);
}
}
/* 创建半透明变体 */
.overlay {
/* 品牌色 40% 不透明度 */
background: color-mix(in srgb, var(--brand) 40%, transparent);
}色彩空间
css
/* color-mix(in <color-space>, color1 percentage, color2 percentage) */
/* sRGB:最常用,符合直觉 */
color-mix(in srgb, blue 50%, red)
/* OKLCH:感知均匀,混合结果更自然 */
color-mix(in oklch, blue 50%, red)
/* 对比:sRGB 混合蓝+黄可能偏灰,OKLCH 混合更鲜艳 */
/*
推荐:
- 日常使用 srgb
- 需要更自然渐变/调色时用 oklch
*/light-dark() 与主题切换
css
/* 定义颜色方案 */
:root {
color-scheme: light dark;
}
/* light-dark(浅色值, 深色值) */
body {
background: light-dark(#ffffff, #1a1a2e);
color: light-dark(#333333, #eeeeee);
}
.card {
background: light-dark(#f5f5f5, #2d2d44);
border: 1px solid light-dark(#e0e0e0, #444);
}
/* 比 prefers-color-scheme 更简洁 */
/* 之前需要这样写:*/
.card {
background: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
.card { background: #2d2d44; }
}
/* 现在一行搞定 */
.card {
background: light-dark(#f5f5f5, #2d2d44);
}@property 自定义属性注册
允许为 CSS 自定义属性声明类型、继承行为和初始值,解锁自定义属性的动画能力:
css
/* 注册一个有类型的自定义属性 */
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --color-start {
syntax: '<color>';
inherits: false;
initial-value: #4a90d9;
}
/* 现在可以对自定义属性做 transition/animation! */
.gradient-border {
--gradient-angle: 0deg;
border: 3px solid transparent;
background:
linear-gradient(white, white) padding-box,
linear-gradient(var(--gradient-angle), #4a90d9, #e74c3c) border-box;
transition: --gradient-angle 0.5s;
}
.gradient-border:hover {
--gradient-angle: 180deg;
}
/* 旋转渐变动画 */
@keyframes rotate-gradient {
to { --gradient-angle: 360deg; }
}
.animated-gradient {
animation: rotate-gradient 3s linear infinite;
}为什么需要 @property?
css
/* 没有 @property 时,自定义属性是"无类型字符串" */
/* 浏览器不知道如何插值,transition 不生效 */
.box {
--my-color: red;
background: var(--my-color);
transition: --my-color 0.3s; /* ❌ 不生效 */
}
/* 注册后,浏览器知道这是 <color>,可以插值 */
@property --my-color {
syntax: '<color>';
inherits: false;
initial-value: red;
}
.box {
--my-color: red;
background: var(--my-color);
transition: --my-color 0.3s; /* ✅ 平滑过渡 */
}
.box:hover {
--my-color: blue;
}@starting-style 入场动画
解决了长期以来 CSS 无法对 display: none → display: block 做动画的问题:
css
dialog {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s, display 0.3s allow-discrete;
}
/* 定义"起始样式"——元素刚出现时的状态 */
@starting-style {
dialog {
opacity: 0;
transform: translateY(-20px);
}
}
/* 关闭时的样式(过渡到此状态后隐藏) */
dialog:not([open]) {
opacity: 0;
transform: translateY(-20px);
display: none;
}之前 vs 现在
之前(需要 JS + 额外 class):
1. JS 添加 .visible class
2. 使用 requestAnimationFrame 确保浏览器已渲染初始状态
3. JS 添加 .show class 触发动画
4. 关闭时 JS 移除 .show,监听 transitionend 再移除 .visible
现在(纯 CSS):
1. @starting-style 定义入场初始状态
2. allow-discrete 允许离散属性(display)参与过渡
3. 浏览器自动处理 display: none ↔ block 的动画Popover + @starting-style
css
[popover] {
opacity: 1;
transform: scale(1);
transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
[popover]:not(:popover-open) {
opacity: 0;
transform: scale(0.95);
}View Transitions 视图过渡
实现页面或组件之间的平滑过渡动画:
同文档视图过渡(SPA)
js
// JS 触发视图过渡
document.startViewTransition(() => {
updateDOM(); // 同步修改 DOM
});css
/* 自定义过渡动画 */
::view-transition-old(root) {
animation: fade-out 0.3s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
/* 给特定元素命名,实现独立过渡 */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero) {
animation: scale-down 0.4s ease-in-out;
}
::view-transition-new(hero) {
animation: scale-up 0.4s ease-in-out;
}跨文档视图过渡(MPA)
css
/* 在两个页面中都声明 */
@view-transition {
navigation: auto;
}
/* 源页面的离场动画 */
::view-transition-old(root) {
animation: slide-out-left 0.3s;
}
/* 目标页面的入场动画 */
::view-transition-new(root) {
animation: slide-in-right 0.3s;
}:is() 和 :where()
:is() — 简化复杂选择器
css
/* 之前 */
header a:hover,
nav a:hover,
footer a:hover {
color: #4a90d9;
}
/* 之后 */
:is(header, nav, footer) a:hover {
color: #4a90d9;
}
/* 嵌套简化 */
.card :is(h1, h2, h3, h4) {
font-weight: bold;
line-height: 1.2;
}:where() — 零特异性版本
css
/* :is() 的特异性 = 参数中最高的那个 */
:is(.card, #main) a { } /* 特异性 = 1-0-1(取 #main 的) */
/* :where() 的特异性永远为 0 */
:where(.card, #main) a { } /* 特异性 = 0-0-1 */
/* 实际用途:写可被轻松覆盖的基础样式 */
:where(.btn) {
padding: 8px 16px;
border-radius: 4px;
}
/* 用户自定义样式可以用简单选择器覆盖 */
.my-btn {
padding: 12px 24px; /* ✅ 轻松覆盖 :where 的样式 */
}Subgrid 子网格
让子元素参与父网格的轨道对齐:
css
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.card {
display: grid;
/* 子网格继承父网格的行轨道 */
grid-row: span 3;
grid-template-rows: subgrid;
gap: 8px;
}
/*
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Title │ │ Title │ │ Title │ ← 行 1 对齐
│──────────│ │──────────│ │──────────│
│ Content │ │ Content │ │ Content │ ← 行 2 对齐
│ varies │ │ │ │ more │
│──────────│ │──────────│ │──────────│
│ Footer │ │ Footer │ │ Footer │ ← 行 3 对齐
└──────────┘ └──────────┘ └──────────┘
没有 subgrid 时,每个 card 的内部行高独立,Footer 无法对齐
有 subgrid 后,card 的行轨道由父网格统一控制
*/其他值得关注的新特性
text-wrap: balance / pretty
css
/* balance:让多行标题的每行长度尽量相等 */
h1, h2, h3 {
text-wrap: balance;
}
/* pretty:避免最后一行只有一个孤单的单词 */
p {
text-wrap: pretty;
}field-sizing
css
/* 让 textarea/input 根据内容自动调整大小 */
textarea {
field-sizing: content;
min-height: 3lh; /* 最小 3 行高 */
max-height: 10lh; /* 最大 10 行高 */
}interpolate-size
css
/* 允许 height: auto 参与过渡动画 */
:root {
interpolate-size: allow-keywords;
}
.collapsible {
height: 0;
overflow: hidden;
transition: height 0.3s;
}
.collapsible.open {
height: auto; /* 之前无法过渡到 auto,现在可以了! */
}anchor() 锚点定位
css
/* 声明锚点 */
.trigger {
anchor-name: --my-trigger;
}
/* 相对于锚点定位 */
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* 定位在锚点下方居中 */
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
/* 自动翻转(空间不足时) */
position-try-fallbacks: flip-block;
}浏览器兼容策略
@supports 渐进增强
css
/* 基础样式(所有浏览器) */
.layout {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* 支持容器查询时增强 */
@supports (container-type: inline-size) {
.sidebar {
container-type: inline-size;
}
@container (min-width: 300px) {
.widget { display: grid; grid-template-columns: 1fr 1fr; }
}
}
/* 支持 :has() 时增强 */
@supports selector(:has(*)) {
form:has(:invalid) .submit-btn {
opacity: 0.5;
}
}兼容性速查
| 特性 | Chrome | Firefox | Safari | 可用度 |
|---|---|---|---|---|
@layer | 99+ | 97+ | 15.4+ | ✅ 放心用 |
:has() | 105+ | 121+ | 15.4+ | ✅ 放心用 |
@container | 105+ | 110+ | 16+ | ✅ 放心用 |
| CSS Nesting | 120+ | 117+ | 17.2+ | ✅ 放心用 |
color-mix() | 111+ | 113+ | 16.2+ | ✅ 放心用 |
@property | 85+ | 128+ | 15.4+ | ✅ 放心用 |
light-dark() | 123+ | 120+ | 17.5+ | ⚠️ 注意旧版 |
@starting-style | 117+ | ❌ | 17.5+ | ⚠️ 渐进增强 |
| View Transitions | 111+ | ❌ | 18+ | ⚠️ 渐进增强 |
@view-transition (MPA) | 126+ | ❌ | ❌ | ⚠️ 仅 Chromium |
| Subgrid | 117+ | 71+ | 16+ | ✅ 放心用 |
anchor() | 125+ | ❌ | ❌ | ⚠️ 仅 Chromium |
interpolate-size | 129+ | ❌ | ❌ | ⚠️ 仅 Chromium |
field-sizing | 123+ | ❌ | ❌ | ⚠️ 仅 Chromium |
面试高频题
1. @layer 解决了什么问题?层的优先级规则是怎样的?
@layer 解决的是 CSS 级联(Cascade)优先级失控的问题。传统 CSS 中,第三方库的高特异性选择器常常覆盖开发者自己的样式,迫使开发者使用 !important 或增加选择器复杂度。@layer 允许显式声明层的优先级顺序:①后声明的层优先级高于先声明的层;②未分层的样式优先级高于所有层;③!important 在层中的优先级反转(低优先级层的 !important 反而胜出)。实际应用中常将第三方库放入低优先级层,确保业务样式总能覆盖。
2. :has() 选择器能做什么?为什么被称为"CSS 的 if 语句"?
:has() 是关系选择器,可以根据元素的后代、子元素或后续兄弟元素的状态来选中当前元素。之所以被称为"CSS 的 if",是因为它实现了"如果包含 X 则应用样式"的条件逻辑——这在以前只能通过 JS 实现。典型场景:①表单验证 label:has(+ input:invalid) 让标签变红;②body:has(dialog[open]) 给背景加模糊;③.layout:has(.sidebar) 切换布局模式。它不能向上穿越 Shadow DOM,且大量使用时需注意性能。
3. 容器查询与媒体查询的核心区别是什么?
媒体查询基于视口(viewport)尺寸,容器查询基于父容器尺寸。核心区别:①媒体查询是全局的,同一组件在不同容器中响应相同的断点;容器查询是局部的,组件根据自身所在容器的大小自适应。②使用容器查询需要先用 container-type 声明查询容器。③容器查询还引入了新单位(cqw/cqh/cqi)和样式查询(@container style(--variant: featured))。④容器查询使组件真正自包含——同一组件放入侧边栏(300px)和主内容区(900px)时自动切换布局。
4. @property 注册自定义属性的作用是什么?
@property 为 CSS 自定义属性声明 syntax(类型)、inherits(是否继承) 和 initial-value(初始值)。最核心的作用是让自定义属性支持 transition 和 animation。未注册的自定义属性是无类型字符串,浏览器不知道如何插值(如从 red 到 blue),所以 transition 不生效。注册后浏览器知道这是 <color> 类型,就能做平滑过渡。典型应用:渐变角度动画(syntax: '<angle>')、颜色过渡、进度条百分比动画。
5. @starting-style 解决了什么长期痛点?
CSS 长期无法对 display: none → display: block 做动画——因为元素从不存在到存在,浏览器无法确定动画的"起始状态"。@starting-style 就是用来声明"元素首次显示时的初始样式",配合 transition: display 0.3s allow-discrete(allow-discrete 允许离散属性参与过渡),浏览器就知道从 @starting-style 中的状态过渡到正常状态。这替代了以前需要 JS + requestAnimationFrame + 额外 class 的 hack 方案,对 dialog、popover 等原生弹出元素尤其有用。
追问思考
- Tailwind CSS v4 如何利用
@layer来组织其生成的 CSS?这对自定义覆盖有什么影响? :has()的性能表现如何?浏览器是怎样优化大规模:has()查询的?什么场景下需要注意性能?- 容器查询的
container-type: inline-size会在元素上创建新的 包含上下文(Containment Context),这对子元素的百分比计算有什么影响? - View Transitions API 在 SPA 框架(如 Next.js App Router)中如何与路由系统集成?有哪些限制?
- 原生 CSS Nesting 已经成熟,是否意味着 Sass/Less 预处理器不再需要了?预处理器还有哪些不可替代的能力?