外观
现代 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 都应该独立于实现存在,否则换方案时成本极高。
常见坑
- 容器查询滥用。给所有元素加
container-type会引入额外的布局约束,只给真正需要响应式布局的容器加。 :has()选择器过重。在高频更新的列表里使用复杂:has(),要实测性能。- 原子化类名动态拼接。
bg-${color}-500这类写法不会被扫描到,改用映射表返回完整类名。 - 忽略级联层。第三方样式和自定义样式混在一起时,用
@layer明确优先级,而不是加!important。 - 只用媒体查询做组件响应式。组件被复用到不同容器时布局会错,容器查询更合适。
小结
现代 CSS 的方向是把能力下沉到浏览器:组件级响应式、父级选择、嵌套和级联层都不再必须依赖预处理器或 JavaScript。原子化方案解决的是开发效率和设计约束,不是 CSS 能力问题。两者可以并存,关键是让设计 token 和组件边界保持清晰。