Skip to content

现代 CSS:容器查询、:has() 与原子化方案 ​

CSS 在过去几年补齐了一批长期缺失的能力:组件级响应式、父选择器、原生嵌套、子网格、颜色混合。同时,Tailwind 这类原子化方案改变了团队写样式的方式。

这篇文章聚焦三件事:容器查询如何改变组件设计、:has() 能解决什么问题,以及原子化 CSS 在工程上的真实取舍。

容器查询:让组件自己决定布局 ​

媒体查询关注视口宽度,但组件被放进侧边栏、弹窗或主内容区时,视口宽度并不能反映它的可用空间。

容器查询让样式依赖父容器尺寸:

css
.card-list {
  container-type: inline-size;
  container-name: cards;
}

.card {
  display: block;
}

@container cards (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 120px 1fr;
    gap: 16px;
  }
}

container-type: inline-size 声明容器在行内方向可被查询。@container cards (...) 根据最近的命名容器决定样式。

容器查询单位也很实用:

css
.card__title {
  font-size: clamp(1rem, 2.5cqw, 1.5rem);
}

cqw 是容器宽度的百分比,组件被放入不同宽度的容器时,字号会自动调整。

使用时注意两点:

  • container-type: inline-size 会创建包含块和布局约束,可能影响绝对定位和内部布局,接入前检查现有样式。
  • 容器查询解决组件内部布局,页面级断点仍应使用媒体查询,两者不是替代关系。

:has():终于能选择"包含某元素的父级" ​

:has() 被称为"父选择器",它允许根据后代或兄弟节点匹配元素:

css
/* 表单有非法输入时,给整个字段加错误态 */
.form-field:has(input:invalid) {
  border-color: #dc2626;
}

/* 卡片包含图片时,调整内边距 */
.card:has(> img) {
  padding-top: 0;
}

/* 选中某项后,隐藏空状态 */
.list:has(.list-item:checked) .empty-state {
  display: none;
}

它也能减少 JavaScript 状态:

css
/* 没有勾选任何复选框时,批量操作栏禁用 */
.toolbar:not(:has(input[type='checkbox']:checked)) {
  opacity: 0.5;
  pointer-events: none;
}

:has() 已经进入现代浏览器基线。性能上不必过度担心,但不要写过于复杂的选择器,尤其在大型列表和高频更新的 DOM 上。

原生嵌套与其它新能力 ​

原生 CSS 嵌套已经可用,写法接近 Sass:

css
.card {
  padding: 16px;

  &__title {
    font-weight: 600;
  }

  &:hover {
    border-color: var(--brand-color);
  }

  @media (width >= 768px) {
    padding: 24px;
  }
}

&__title 这种 BEM 写法在原生嵌套中需要谨慎:& 代表父选择器,拼接后可能生成预期外的选择器。团队可以统一使用 & .card__title 或直接写完整类名。

其它值得关注的能力:

  • aspect-ratio:为图片、视频和占位容器预留比例,减少 CLS。
  • clamp():一行代码实现流式字号和间距。
  • color-mix():在 CSS 中混合颜色,方便生成主题色阶。
  • text-wrap: balance:让标题换行更均衡。
  • @layer:显式控制级联层级,解决第三方样式覆盖问题。
  • 子网格 subgrid:让嵌套网格共享父级轨道。
css
@layer reset, base, components, utilities;

@layer components {
  .button {
    background: color-mix(in oklab, var(--brand-color) 85%, black);
  }
}

@layer 特别适合设计系统:基础样式、组件样式、工具类的优先级可以显式声明,不再靠提高选择器权重互相覆盖。

原子化 CSS 的工程取舍 ​

Tailwind 的核心是把样式拆成最小粒度工具类,直接在模板中组合:

html
<button class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500">
  提交
</button>

它的优势很明确:

  • 没有命名成本,不需要在多个文件间跳转。
  • 样式与结构在同一个位置,改组件时不容易漏改。
  • 产物体积通过扫描模板自动裁剪,通常小于手写 CSS。
  • 设计约束内置于工具类,减少随意取值。

代价同样明显:

  • 模板变长,复杂组件需要抽取组件或使用 @apply。
  • 动态拼接类名可能被裁剪掉,必须使用完整类名或 safelist。
  • 团队需要统一设计 token,否则会出现大量任意值 w-[137px]。
  • 调试时看到的是工具类,定位具体样式来源不如语义类直观。

Tailwind 4 改为 CSS 优先配置,不再强制依赖 tailwind.config.js:

css
@import 'tailwindcss';

@theme {
  --color-brand-500: oklch(0.55 0.2 270);
  --font-sans: 'Inter', 'PingFang SC', sans-serif;
}

Vite 中接入:

ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()]
})

方案对比 ​

方案优点代价适合场景
原生 CSS / CSS Modules无额外依赖,作用域清晰命名与组织靠约定大多数业务组件
Sass / Less嵌套、变量、mixin 成熟需要编译,语言能力逐渐被 CSS 补齐存量项目
Tailwind开发快,约束统一,产物体积小模板冗长,动态类名受限组件化程度高的产品
CSS-in-JS样式与组件状态深度结合运行时开销、SSR 复杂度强动态主题的 React 应用

选型时优先考虑团队的样式治理能力,而不是工具本身。无论用哪种方案,设计 token 都应该独立于实现存在,否则换方案时成本极高。

常见坑 ​

  1. 容器查询滥用。给所有元素加 container-type 会引入额外的布局约束,只给真正需要响应式布局的容器加。
  2. :has() 选择器过重。在高频更新的列表里使用复杂 :has(),要实测性能。
  3. 原子化类名动态拼接。bg-${color}-500 这类写法不会被扫描到,改用映射表返回完整类名。
  4. 忽略级联层。第三方样式和自定义样式混在一起时,用 @layer 明确优先级,而不是加 !important。
  5. 只用媒体查询做组件响应式。组件被复用到不同容器时布局会错,容器查询更合适。

小结 ​

现代 CSS 的方向是把能力下沉到浏览器:组件级响应式、父级选择、嵌套和级联层都不再必须依赖预处理器或 JavaScript。原子化方案解决的是开发效率和设计约束,不是 CSS 能力问题。两者可以并存,关键是让设计 token 和组件边界保持清晰。

基于 VitePress 构建 · 持续更新中