Skip to content

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;
  }
}

兼容性速查

特性ChromeFirefoxSafari可用度
@layer99+97+15.4+✅ 放心用
:has()105+121+15.4+✅ 放心用
@container105+110+16+✅ 放心用
CSS Nesting120+117+17.2+✅ 放心用
color-mix()111+113+16.2+✅ 放心用
@property85+128+15.4+✅ 放心用
light-dark()123+120+17.5+⚠️ 注意旧版
@starting-style117+17.5+⚠️ 渐进增强
View Transitions111+18+⚠️ 渐进增强
@view-transition (MPA)126+⚠️ 仅 Chromium
Subgrid117+71+16+✅ 放心用
anchor()125+⚠️ 仅 Chromium
interpolate-size129+⚠️ 仅 Chromium
field-sizing123+⚠️ 仅 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。未注册的自定义属性是无类型字符串,浏览器不知道如何插值(如从 redblue),所以 transition 不生效。注册后浏览器知道这是 <color> 类型,就能做平滑过渡。典型应用:渐变角度动画(syntax: '<angle>')、颜色过渡、进度条百分比动画。

5. @starting-style 解决了什么长期痛点?

CSS 长期无法对 display: none → display: block 做动画——因为元素从不存在到存在,浏览器无法确定动画的"起始状态"。@starting-style 就是用来声明"元素首次显示时的初始样式",配合 transition: display 0.3s allow-discreteallow-discrete 允许离散属性参与过渡),浏览器就知道从 @starting-style 中的状态过渡到正常状态。这替代了以前需要 JS + requestAnimationFrame + 额外 class 的 hack 方案,对 dialogpopover 等原生弹出元素尤其有用。


追问思考

  1. Tailwind CSS v4 如何利用 @layer 来组织其生成的 CSS?这对自定义覆盖有什么影响?
  2. :has() 的性能表现如何?浏览器是怎样优化大规模 :has() 查询的?什么场景下需要注意性能?
  3. 容器查询的 container-type: inline-size 会在元素上创建新的 包含上下文(Containment Context),这对子元素的百分比计算有什么影响?
  4. View Transitions API 在 SPA 框架(如 Next.js App Router)中如何与路由系统集成?有哪些限制?
  5. 原生 CSS Nesting 已经成熟,是否意味着 Sass/Less 预处理器不再需要了?预处理器还有哪些不可替代的能力?

用心学习,用代码说话 💻