样式方案
样式的历史是一部"作用域与复用"的斗争史:全局选择器 → BEM → CSS Modules → 原子化 → 设计令牌。2026 年的合理姿势是四者并存:原生现代 CSS 打底、CSS Modules/组件样式局部化、原子化工具提速、令牌系统统一品牌变量。本文逐一展开,并在最后给出一套可以落地的组合方案。
一、现代 CSS 特性(2026 年的原生能力)
大部分过去需要预处理器/JS 才能做的事,原生 CSS 已经能做:
| 特性 | 解决的问题 | 可用性 |
|---|---|---|
原生嵌套 & | 取代预处理器嵌套 | 2023 起主流浏览器支持 |
@layer | 层叠管理(框架/组件/自定义样式优先级) | 全绿 |
@container + container-type | 容器查询(按组件宽度而非视口响应) | 全绿 |
:has() | 父级选择器("包含某子元素的父元素") | 全绿 |
color-mix() / oklch() | 颜色计算与感知均匀的颜色空间 | 全绿 |
:where() / :is() | 零权重/方便的选择器分组 | 全绿 |
light-dark() | 一套值同时适配明暗(配合 color-scheme) | 2024 起逐步普及 |
text-wrap: balance/pretty | 标题断行均衡、正文断行更优 | 全绿 |
subgrid | 子网格对齐 | 2023 起可用 |
view-transition API | 跨页面/状态过渡 | 全绿(渐进增强) |
/* 原生嵌套:必须有 & 才表示"自身",否则是后代选择器 */
.card {
color: oklch(0.3 0.1 260);
& h2 { font-weight: 600; } /* .card h2 */
&:hover { border-color: var(--accent); }
& > .meta { font-size: 0.85em; }
}
/* :has —— 父级反向选择:含大图的卡片更宽 */
.card:has(> img.hero) { grid-column: span 2; }
/* 容器查询:以 .widget 宽度为断点,而非视口 */
.widget { container-type: inline-size; }
@container (min-width: 480px) { .widget { display: grid; grid-template-columns: 1fr auto; } }坑 1:原生嵌套写
&:hover容易忘&前缀的语义差异(不带&是后代选择器);嵌套过深会显著增加选择器复杂度——保持 1~2 层。 坑 2:color-mix()、oklch()与现代特性配合渐进增强:旧浏览器给兜底值,新浏览器用@supports升级,而不是反向。
二、CSS Modules 与框架局部样式
把样式作用域收到组件,避免全局污染与命名苦役:
/* Button.module.css */
.root { display: inline-flex; gap: .5rem; }
.primary { background: var(--brand); }// React(Vite 开箱支持 *.module.css)
import styles from './Button.module.css'
export function Button({ variant = 'primary' }) {
return <button className={`${styles.root} ${styles[variant]}`}>确定</button>
}| 方案 | 机制 | 适用 |
|---|---|---|
| CSS Modules | 编译期为类名加哈希 | React/Vue SFC 均可,配合组件化 |
Vue <style scoped> | 属性选择器(data-v-xxx)限域 | Vue SFC 内建 |
| CSS-in-JS(vanilla-extract) | 构建期把 TS 编译成静态 CSS | 想用变量/主题推导又不想运行时成本 |
/* Vue scoped 编译示意:.title[data-v-abc123] { ... } —— 依赖子元素能命中属性 */
:deep(.child) { color: red; } /* 穿透 scoped:样式作用到子组件内部根元素之外时用 */坑 3:Vue scoped 对子组件内部默认不透出——需要
:deep();React CSS Modules 对第三方库内部样式无能为力,要覆盖库样式得靠全局层 +@layer管理优先级(见 §5)。
三、原子化(Utility-First):Tailwind CSS 与 UnoCSS
3.1 原子化为什么流行
把"一条样式规则"抽象成单一用途的 class:flex、p-4、text-sm。收益:不再起类名、天然避免死 CSS、改动只动模板;代价是学习一套速记体系,并接受"样式出现在模板里"。
3.2 Tailwind CSS v4(CSS-first 配置)
v4 起配置从 tailwind.config.js 迁到 CSS 内,内置 Rust 引擎,用 @import "tailwindcss" 单行启用:
/* app.css —— Tailwind v4 全配置就是 CSS */
@import "tailwindcss";
@theme {
--color-brand: #4f46e5; /* 定义设计令牌 → 生成 bg-brand / text-brand 等 */
--font-sans: "Inter", ui-sans-serif, system-ui;
}
@custom-variant dark (&:where(.dark, .dark *)); /* 自定义暗色变体(默认媒体查询或 .dark) */<button class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand/90">
提交
</button>- 变体:
hover:、dark:、md:、group-hover:、has-[:checked]:等; - 任意值:
grid-cols-[200px_1fr]、max-w-[theme(--breakpoint-lg)]; bg-brand/90透明度语法直接消费--color-*令牌。
3.3 UnoCSS:按需生成的原子化引擎
UnoCSS 不是"又一个 Tailwind",而是可编程的原子化生成器:只按使用到的 class 生成 CSS(体积最小化),规则可用 preset 或自定义:
// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss'
export default defineConfig({
presets: [presetUno()], // Tailwind/Wind 兼容子集
rules: [
['bg-brand', { background: 'var(--color-brand)' }],
[/^m-(\d+)$/, ([, n]) => ({ margin: `${Number(n) * 4}px` })], // 自定义动态规则
],
shortcuts: { 'card-shell': 'rounded-lg border border-gray-200 shadow-sm p-4' },
})<div card-shell flex="~ items-center justify-between">
<span text-xl font-medium>标题</span>
</div>Tailwind vs UnoCSS 一句话:工程基建偏重/想 Vite 插件即插即用 → UnoCSS 更轻;要生态与文档成熟度、团队标准统一 → Tailwind 更稳。两者都会做"扫描 + 按需生成",不要再手动维护一份 utility 长 CSS。
四、设计令牌与主题系统
4.1 三层令牌模型
| 层 | 例子 | 用途 |
|---|---|---|
| 基础令牌(primitive) | --gray-100: #f4f4f5 | 品牌原始色板、间距、圆角尺寸 |
| 语义令牌(alias) | --color-surface: var(--gray-100)、--color-text-emphasis | 组件只消费语义名,换肤只改映射 |
| 组件令牌 | --btn-bg: var(--color-primary) | 组件内引用,允许组件级覆盖 |
/* tokens.css */
:root {
--color-primary: #4f46e5;
--color-bg: #fff;
--color-text: #18181b;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root { --color-bg: #09090b; --color-text: #fafafa; color-scheme: dark; }
}
/* light-dark() 让组件不用写两遍(需要 color-scheme 生效) */
.card { background: light-dark(#fff, #18181b); color: light-dark(#18181b, #fafafa); }坑 4:token 别直连品牌数值写进组件——组件只认语义 token,否则"换肤/暗色/多品牌"时要全局改组件。这是设计系统里最高频返工点。 坑 5:暗色用媒体查询切换后,页面根上
background要同步(否则原生控件、color-scheme与滚动条仍是浅色)。
4.2 框架落地
- Tailwind v4:
@theme即令牌源,暗色变体默认跟随系统/自定义.dark; - UnoCSS:
theme预设对象 + 变体插件; - 组件库联动:把语义 CSS 变量挂到
:root,框架层(MUI/AntD/Element Plus 的主题算法)也映射同一批变量,避免两套色板漂移。
五、可落地的组合方案
- 基线:
@layer把优先级显式化——reset < tokens < components < utilities,Tailwind v4 的@import "tailwindcss"自带分层; - 组件样式:CSS Modules(React)/
<style scoped>(Vue),复杂组件内部用现代特性写原生 CSS; - 快速布局与一次性样式:原子化(工程选 Tailwind 或 UnoCSS);
- 品牌/主题:语义 CSS 变量 + 明暗
color-scheme;需要 JS 动态换肤时提供data-theme属性切换变量集; - 收尾检查:构建产物里不应出现"没用到的 CSS"(原子化引擎保证),也不应出现两条规则抢同一个优先级而靠
!important压(用@layer排序代替)。
六、常见坑速查
- 原生嵌套忘
&(§1 坑 1);2. 无兜底地使用新特性(渐进增强方向反了); - scoped/Modules 管不到子组件与第三方库内部(用
:deep/@layer全局层); - token 与色板两套体系漂移(组件直写品牌数值);
- 暗色只改文字不改
color-scheme,原生控件刺眼; - 原子化与组件样式互相覆盖:把"不可覆盖"(品牌规则)放后加载的 layer,方向反了必打架;
- 过度抽象 utility 化业务:把
card-hover-shadow-2之类一次性语义硬塞进 shortcuts,得不偿失; !important满地走:优先级战争是设计问题,用@layer顺序解决。
状态与参考
- 状态:已收录(2026-09-02,由前端领域规划清单「样式方案|Tailwind / UnoCSS、CSS Modules、设计令牌」转正式)。
- 版本:现代 CSS 特性均为 2026 年主流稳定能力(嵌套/
@layer/容器查询/:has()/color-mix()等);Tailwind v4 为 CSS-first 配置范式;UnoCSS 示例为当前推荐 API。示例为标准用法,复制到 Vite 工程(+ @tailwindcss/vite 或 unocss/vite)即可运行;版本细节以官方文档为准。 - 参考:Tailwind CSS 官方文档(v4 安装与
@theme)、UnoCSS、MDN CSS 参考(嵌套/容器查询/color-mix)、Style Dictionary(令牌)。 - 阅读联动:与构建/体积优化的关系见构建工具与性能优化(CLS/字体/图片);设计令牌也可对照 Vue 3 生态 中组件库章节。
下一步
- [ ] 用一个 Vite + Vue/React 工程把 §5 组合方案完整落地并跑构建(含 @layer 优先级验证)
- [ ] 补一篇"语义化 CSS vs 原子化 vs 混合"的团队落地决策笔记
- [ ] 对照主题系统写暗色/多主题实现对比(CSS 变量 vs
light-dark()vs 框架主题算法)
写作规范请参阅领域概览。