现代 CSS 里那些被低估的特性

我们已经不需要那么多媒体查询了

响应式设计在过去十年里几乎等同于媒体查询。但媒体查询有一个根本问题:它只能感知视口宽度,不知道组件自己被放在了多宽的空间里。

一个卡片组件放在侧边栏(300px)和放在主区域(800px)时,理想布局完全不同,而媒体查询对此无能为力——除非你为每个使用场景写一套专用的类名。

容器查询

容器查询让组件感知自己的容器:

css
/* 1) 声明这是一个可查询的容器 */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

/* 2) 组件根据自己的容器宽度调整 */
.card {
  display: grid;
  gap: 12px;
}

@container card (min-width: 520px) {
  .card {
    grid-template-columns: 200px 1fr;
    align-items: start;
  }
}

@container card (min-width: 800px) {
  .card {
    grid-template-columns: 260px 1fr auto;
  }
}

这一段 CSS 的价值在于:卡片组件现在是真正自包含的。同一个 .card 类,放进窄容器就竖排,放进宽容器就横排,不需要任何外部干预。

:has() —— 终于能选父元素了

:has() 被称为"父选择器",但实际上它的能力远不止于此:

css
/* 表单:有输入时才高亮整个字段组 */
.field:has(input:focus) {
  outline: 2px solid var(--brand);
}

/* 有图片的文章卡片,标题收紧一点 */
.post-card:has(img) .card-title {
  margin-bottom: 6px;
}

/* 数量为 0 时隐藏整个区块 */
.list:not(:has(li))::after {
  content: "暂无内容";
  color: var(--text-mute);
}

/* 前面紧跟标题的段落,去掉上边距 */
h2:has(+ p) {
  margin-bottom: 0.4em;
}

最后那个例子在过去必须用 JavaScript 才能做——遍历所有标题,检查下一个兄弟节点是不是段落,再加类名。现在一行 CSS 就完成了,而且窗口 resize、内容动态插入时都会自动更新。

color-mix() 与相对颜色

以前要做一个"主色的 10% 透明版本",你得手动算出 rgba 值,主色一改就全部失效:

css
/* 旧写法:主色变了这里就不同步了 */
--brand-soft: rgba(246, 130, 31, 0.1);

/* 新写法:从主色推导,永远同步 */
--brand-soft: color-mix(in srgb, var(--brand) 12%, transparent);

/* 半透明背景,用于毛玻璃顶栏 */
.site-header {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
}

这让主题系统变得极其好维护:只需要定义几个基础色,其余几十个派生色全部自动计算。改一次 --brand,整站配色跟着变。

逻辑属性:一份 CSS 服务所有书写方向

物理属性(margin-left)在从右往左的语言里会失效。逻辑属性用书写方向代替物理方向:

css
/* 旧:阿拉伯语环境下会错位 */
.entry {
  margin-left: 24px;
  border-left: 3px solid var(--brand);
  padding-right: 16px;
}

/* 新:自动适配 LTR / RTL / 竖排 */
.entry {
  margin-inline-start: 24px;
  border-inline-start: 3px solid var(--brand);
  padding-inline-end: 16px;
}

对只做中文/英文站点的项目,这看起来没必要。但一旦你要支持阿拉伯语或希伯来语,改写成本从"重写整个样式表"降到"几乎为零"。

一些语法糖

嵌套

css
.post-card {
  padding: 24px;

  &:hover {
    transform: translateY(-3px);
  }

  .card-title {
    margin: 0;
  }
}

原生嵌套已经是标准,不再需要 Sass 或 PostCSS 来编译这一层。

clamp() 做流体排版

css
/* 视口 375px 时约 28px,1440px 时约 40px,中间线性插值 */
.post-title {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
}

一行替代了过去三四个断点的手写媒体查询,而且任意视口宽度下都是连续变化的,没有断点处的跳变。

一个实用的组合:主题切换

把这些特性组合起来,实现一个跟随系统 + 手动覆盖的主题系统:

css
:root {
  color-scheme: light;
  --bg: #ffffff;
  --text: #1a1d23;
  --brand: #f6821f;
}

/* 跟随系统 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #0f1115;
    --text: #e8eaee;
    --brand: #ff9d4d;
  }
}

/* 手动强制 */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0f1115;
  --text: #e8eaee;
  --brand: #ff9d4d;
}

关键点是 :root:not([data-theme="light"]) 这个选择器:它让"跟随系统"成为默认行为,同时允许手动指定 light 来覆盖系统偏好。color-scheme 属性则告诉浏览器该用哪种原生控件配色(滚动条、表单控件、下拉框),这一步经常被漏掉,导致深色模式下出现刺眼的白底滚动条。

小结

特性解决的问题替代掉的方案
容器查询组件感知自身空间为每个场景写专用类名
:has()根据子/兄弟元素选父元素JavaScript 遍历加类
color-mix()从基础色推导派生色手写 rgba 常量
逻辑属性书写方向自适应为 RTL 重写样式表
原生嵌套样式层级组织Sass / PostCSS
clamp()流体排版多个断点媒体查询

这些特性的共同点是:它们把过去必须靠构建工具或 JavaScript 完成的事,变成了浏览器原生能力。前端工具链越来越复杂的同时,CSS 本身其实一直在悄悄变强。