vanilla-extract
使用 TypeScript 编写并在构建期输出静态 CSS 的样式方案
vanilla-extract 把 TypeScript 当作样式预处理器,适合构建大型设计系统、组件库和强调类型安全的前端工程。
vanilla-extract
使用 TypeScript 编写样式,在构建期生成静态 CSS 文件的零运行时样式解决方案。
技术简介说明
vanilla-extract 是一个零运行时的 TypeScript CSS 方案,由 CSS Modules 的共同发明者 Mark Dalgleish 创建。它的核心理念是"将 TypeScript 作为预处理器"——开发者在 .css.ts 文件中用 TypeScript 编写样式定义,构建工具在编译期将这些文件转换为标准 CSS 文件输出,浏览器端不执行任何样式相关的 JavaScript 代码。
vanilla-extract 诞生于 SEEK(澳大利亚大型招聘平台)内部对类型安全样式方案的探索,是 Mark Dalgleish 从 CSS Modules → Treat → vanilla-extract 这一演进路线的最终产物。它填补了 CSS Modules(缺乏类型安全和设计 Token 管理)与运行时 CSS-in-JS(性能开销大、不兼容 RSC)之间的空白。
作为 2025-2026 年最成熟的零运行时 TypeScript CSS 方案之一,vanilla-extract 特别适合构建大型设计系统和组件库。它提供了完整的类型安全体验,包括主题合约、设计 Token、CSS 变量、原子化 CSS(通过 Sprinkles 扩展)等能力,同时保持零运行时的性能优势。
基本信息
- 官网:https://vanilla-extract.style/
- GitHub:https://github.com/vanilla-extract-css/vanilla-extract (⭐ ~10,400+)
- License:MIT
- 最新版本:v1.21.1(核心包
@vanilla-extract/css) - 主要维护者/公司:Mark Dalgleish(CSS Modules 发明者之一)、mattcompiles(Matt Culp);最初由 SEEK(seek-oss)孵化,后迁移至独立组织 vanilla-extract-css
快速上手
安装
以 Vite 项目为例:
# 安装核心包和 Vite 插件
npm install @vanilla-extract/css
npm install --save-dev @vanilla-extract/vite-plugin其他构建工具:
# Webpack
npm install --save-dev @vanilla-extract/webpack-plugin
# esbuild
npm install --save-dev @vanilla-extract/esbuild-plugin
# Next.js
npm install --save-dev @vanilla-extract/next-plugin基础配置
Vite 配置(vite.config.ts):
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
})Next.js 配置(next.config.js):
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin')
module.exports = createVanillaExtractPlugin()({
// 其他 Next.js 配置
})Webpack 配置(webpack.config.js):
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
module.exports = {
plugins: [new VanillaExtractPlugin()],
}最小示例
创建样式文件 styles.css.ts:
import { style } from '@vanilla-extract/css'
export const container = style({
display: 'flex',
flexDirection: 'column',
padding: '16px',
backgroundColor: '#f5f5f5',
})
export const title = style({
fontSize: '24px',
fontWeight: 'bold',
color: '#333',
})
export const button = style({
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
':hover': {
backgroundColor: '#0056b3',
},
})在组件中使用:
import { container, title, button } from './styles.css'
function App() {
return (
<div className={container}>
<h1 className={title}>Hello vanilla-extract</h1>
<button className={button}>点击我</button>
</div>
)
}构建后,.css.ts 文件被编译为标准 CSS 文件,导出的类名是带有局部作用域的哈希字符串(如 styles_container_x2j4a),避免了全局命名冲突。
核心概念与架构
工作原理
┌──────────────────┐ ┌────────────────────┐ ┌──────────────┐
│ .css.ts 文件 │ ──▶ │ 构建时编译器 │ ──▶ │ 静态 .css │
│ (TypeScript) │ │ (Bundler 插件) │ │ 文件输出 │
└──────────────────┘ └────────────────────┘ └──────────────┘
│ │
▼ ▼
导出类名字符串 提取所有样式为
(如 "base_x2j4a") 标准 CSS 文件
- 编写样式:在
.css.ts文件中用 TypeScript 编写样式定义 - 构建处理:Bundler 插件拦截
.css.ts文件,在构建时执行 TypeScript 求值 - CSS 提取:所有样式被提取为标准 CSS 文件
- 类名映射:
.css.ts文件导出局部作用域的类名字符串 - 消费样式:在组件中导入类名字符串并应用到元素
关键约束:所有样式必须在构建时可静态分析,不支持运行时动态生成样式。.css.ts 文件中不能引用运行时变量。
核心 API
| API | 用途 | 说明 |
|---|---|---|
style() | 创建局部作用域样式类 | 最基础的样式创建方式 |
styleVariants() | 创建样式变体映射 | 从对象生成多个样式类 |
globalStyle() | 创建全局样式 | 用于重置样式或覆盖第三方样式 |
createVar() | 创建 CSS 自定义属性 | 返回一个可在样式中引用的变量 |
fallbackVar() | 提供变量降级值 | 当 CSS 变量未定义时使用备选值 |
createTheme() | 创建主题 | 基于主题合约生成主题类 |
createThemeContract() | 创建主题合约 | 定义主题的结构(类型安全的 Token 合约) |
assignVars() | 分配变量值 | 将值赋给主题合约中的变量 |
keyframes() | 定义动画 | 局部作用域的关键帧动画 |
fontFace() | 定义字体 | 局部作用域的字体声明 |
createContainer() | 创建容器查询 | CSS Container Queries 支持 |
layer() | 创建 CSS 层 | CSS Cascade Layers 支持 |
核心特性
- 零运行时(Zero-Runtime):构建时生成标准 CSS 文件,浏览器端无任何 JS 样式引擎开销,与直接编写 CSS 性能一致
- 完整的 TypeScript 类型安全:从主题定义到样式使用,全程享受 IDE 智能提示和编译时类型检查
- 局部作用域类名:自动生成唯一的哈希类名(如
styles_container_x2j4a),彻底避免全局命名冲突 - 一等主题支持:通过
createThemeContract定义类型安全的主题合约,createTheme生成具体主题,实现多主题切换 - CSS 变量原生支持:
createVar创建类型安全的 CSS 自定义属性,fallbackVar提供降级值 - Sprinkles 原子化 CSS:官方扩展包,提供类似 Tailwind 的类型安全原子类体验
- Recipes 变体系统:官方扩展包,用于管理组件样式变体(类似 Stitches recipes)
- 框架无关:可用于 React、Vue、Svelte、Qwik 等任何前端框架,不绑定特定 UI 库
- 完整 CSS 能力:支持选择器、伪类、媒体查询、容器查询、关键帧动画、字体声明、CSS 层等现代 CSS 特性
- 广泛的 Bundler 支持:官方支持 Vite、Webpack、esbuild、Next.js、Remix、Astro、Rollup、Parcel、Gatsby 等主流构建工具
生态图
官方 Bundler 集成
| 集成 | 包名 | 状态 |
|---|---|---|
| Vite | @vanilla-extract/vite-plugin | ✅ 活跃维护(v5.2.4) |
| Webpack | @vanilla-extract/webpack-plugin | ✅ 支持 |
| esbuild | @vanilla-extract/esbuild-plugin | ✅ 支持 |
| Next.js | @vanilla-extract/next-plugin | ✅ 活跃维护 |
| Remix | 通过 Vite 插件 | ✅ 官方文档支持 |
| Astro | @vanilla-extract/astro | ✅ 支持 |
| Rollup | @vanilla-extract/rollup-plugin | ✅ 支持 |
| Parcel | @vanilla-extract/parcel-integration | ✅ 支持 |
| Gatsby | gatsby-plugin-vanilla-extract | ✅ 支持 |
功能扩展包
| 包名 | 功能 | 说明 |
|---|---|---|
@vanilla-extract/sprinkles | 原子化 CSS | 类型安全的原子类工具层,类似 Tailwind 体验 |
@vanilla-extract/recipes | 组件变体 | 管理组件的多变体样式(size、variant 等) |
@vanilla-extract/dynamic | 运行时条件样式 | 有限度支持运行时动态样式(通过 CSS 变量) |
社区生态
| 项目 | 说明 |
|---|---|
| Mantine | 提供 @mantine/vanilla-extract 集成包 |
| Dessert Box | 组件封装方案,类似 Stitches 的 styled |
| Storybook 集成 | 社区维护的 Storybook 集成方案 |
| Rainbow Sprinkles | Sprinkles 的扩展,支持响应式和条件变体 |
关键依赖
- vanilla-extract 本身无重依赖,核心包极轻量
- 构建时依赖 TypeScript 编译器评估
.css.ts文件 - Sprinkles 依赖核心包的 CSS 变量系统
适用场景
- 大型设计系统:vanilla-extract 的类型安全主题合约和设计 Token 管理是构建大型设计系统的理想选择,确保跨团队的一致性
- 组件库开发:局部作用域类名和零运行时特性使其成为开源组件库的优选,不影响使用者的运行时性能
- 企业级应用:TypeScript 类型安全、主题合约和 CSS 变量支持适合需要严格规范的企业级项目
- 设计 Token 驱动的项目:
createThemeContract提供的类型安全 Token 合约,确保设计系统值的正确传递和使用 - React Server Components 项目:零运行时特性天然兼容 RSC,不会引入客户端样式计算
- 多主题应用:通过
createTheme和assignVars实现类型安全的多主题切换(如 light/dark/brand 主题) - 跨框架项目:框架无关的设计使其可在 React、Vue、Svelte 混合使用的 monorepo 中统一样式方案
开发与工程化
开发流程
- 初始化项目:安装 vanilla-extract 和对应的 bundler 插件
- 创建设计 Token:在
.css.ts文件中定义颜色、间距、字体等基础 Token - 创建主题合约:使用
createThemeContract定义主题结构 - 编写组件样式:在
.css.ts文件中为组件编写样式 - 在组件中消费:导入类名字符串应用到 JSX 元素
- 构建部署:bundler 插件自动处理
.css.ts文件,输出标准 CSS
文件组织建议
src/
├── styles/
│ ├── tokens.css.ts # 设计 Token 定义
│ ├── theme.css.ts # 主题合约和主题实现
│ ├── reset.css.ts # 全局重置样式
│ └── vars.css.ts # CSS 变量定义
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.css.ts # 组件样式
│ │ └── Button.recipes.ts # 组件变体(可选)
│ └── Card/
│ ├── Card.tsx
│ └── Card.css.ts
└── sprinkles/
└── sprinkles.css.ts # Sprinkles 原子类定义
CI/CD 集成
- vanilla-extract 与标准 CI/CD 流程兼容,无需特殊配置
- 构建时会自动生成 CSS 文件,确保 CI 环境安装 Node.js 和 TypeScript
- 建议使用
pnpm verify(或pnpm build)在 CI 中验证构建通过 - 注意:TypeScript 类型检查可能增加 CI 构建时间,大型项目需监控
Monorepo 支持
- vanilla-extract 支持 monorepo 中的跨包引用
- 每个包可以有自己的
.css.ts文件 - 共享 Token 和主题可以放在公共包中
性能与安全
性能特点
| 指标 | 表现 |
|---|---|
| 运行时开销 | 零——无 JS 样式引擎,与手写 CSS 性能一致 |
| Bundle 大小 | 仅输出 CSS 文件,JS 中仅包含类名字符串引用 |
| CSS 输出 | 标准 CSS 文件,浏览器高效解析和缓存 |
| Tree-shaking | 未使用的样式通过 bundler 自动排除 |
| SSR 兼容 | 天然兼容,构建时生成 CSS,无 FOUC |
| 构建速度 | 中等——TypeScript 求值有一定开销,但增量编译表现良好 |
性能优化建议
- 使用 Sprinkles 构建原子化基础层——减少 CSS 文件体积,提高类名复用率
- 合理使用
globalStyle——全局样式过多会增加 CSS 文件体积 - 避免过深的样式嵌套——复杂选择器可能影响浏览器渲染性能
- 监控 TypeScript 编译时间——大型项目中
.css.ts文件的 TS 编译可能成为瓶颈
安全注意事项
- vanilla-extract 生成的 CSS 类名是哈希值,不存在 XSS 注入风险
- CSS 变量值是静态的,不存在运行时注入风险
- 使用
globalStyle时注意避免样式污染
已知性能瓶颈
- TypeScript 编译:大量
.css.ts文件的 TS 类型检查可能明显变慢 - HMR 延迟:Vite 下的热更新偶有样式不刷新的问题(社区已知)
- 大型主题合约:过于复杂的
createThemeContract可能增加构建时间
技术对比
| 维度 | vanilla-extract | Panda CSS | Pigment CSS | Kuma UI | Tailwind CSS | CSS Modules |
|---|---|---|---|---|---|---|
| 运行时 | 零 ✅ | 零 ✅ | 零 ✅ | 零 ✅ | 零 ✅ | 零 ✅ |
| 类型安全 | ★★★★★ 完整 TS 类型 | ★★★★★ 完整 TS 类型 | ★★★☆☆ 部分 | ★★★★☆ TS 类型 | ★★★☆☆ 字符串类名 | ★★☆☆☆ 需声明文件 |
| 设计 Token | ★★★★★ 原生合约 | ★★★★★ 原生系统 | ★★★★☆ MUI 主题 | ★★★☆☆ 基础 | ★★★☆☆ 配置文件 | ★☆☆☆☆ 手动管理 |
| 主题切换 | ★★★★★ 类型安全 | ★★★★★ 条件 Token | ★★★★☆ CSS 变量 | ★★★☆☆ 基础 | ★★★★☆ class 切换 | ★☆☆☆☆ 手动 |
| 原子化 CSS | ★★★★☆ 通过 Sprinkles | ★★★★★ 原生 | ★★★★☆ 支持 | ★★★★☆ 支持 | ★★★★★ 原生 | ★☆☆☆☆ 不支持 |
| 学习曲线 | 中等 | 中等 | 低(MUI 用户) | 中等 | 低 | 低 |
| RSC 兼容 | ✅ 完全 | ✅ 完全 | ⚠️ Alpha | ✅ 完全 | ✅ 完全 | ✅ 完全 |
| 框架支持 | 任意框架 | React 为主 | React(MUI) | 仅 React | 任意框架 | 任意框架 |
| npm 周下载量 | ~450K-1M | ~200K | 较小 | ~900 | ~12M | 巨大 |
| 社区规模 | 中(10.4k⭐) | 中(6.1k⭐) | 小(1.1k⭐) | 小(1.9k⭐) | 统治级 | 巨大 |
| 成熟度 | 🟢 成熟 | 🟢 稳定(v1) | 🔴 Alpha/暂停 | 🟡 演进中 | 🟢 成熟 | 🟢 成熟 |
关键对比结论
- vs Tailwind:Tailwind 生态无敌(12M+ 下载),更适合快速原型和小团队;vanilla-extract 更适合需要严格类型安全和设计 Token 管理的大型设计系统
- vs Panda CSS:Panda CSS 增长更快、Chakra UI 团队维护;vanilla-extract 更成熟、框架支持更广(不限 React)
- vs CSS Modules:vanilla-extract 提供 TS 类型安全、设计 Token 合约和主题管理,CSS Modules 更简单直接但缺乏高级能力
- vs 运行时 CSS-in-JS(Emotion/styled-components):零运行时方案在性能和 RSC 兼容性上全面领先
最佳实践
推荐模式
-
使用 Sprinkles 构建原子化基础层
// sprinkles.css.ts import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles' const responsiveProperties = defineProperties({ conditions: { mobile: {}, tablet: { '@media': 'screen and (min-width: 768px)' }, desktop: { '@media': 'screen and (min-width: 1024px)' }, }, defaultCondition: 'mobile', properties: { display: ['none', 'flex', 'block', 'inline'], flexDirection: ['row', 'column'], padding: { none: '0', sm: '4px', md: '8px', lg: '16px' }, }, }) export const sprinkles = createSprinkles(responsiveProperties) -
使用
createThemeContract管理设计 Token// theme.css.ts import { createThemeContract, createTheme, assignVars } from '@vanilla-extract/css' export const themeContract = createThemeContract({ color: { primary: null, secondary: null, text: null, background: null, }, space: { sm: null, md: null, lg: null, }, }) export const lightTheme = createTheme(themeContract, { color: { primary: '#007bff', secondary: '#6c757d', text: '#333', background: '#fff' }, space: { sm: '4px', md: '8px', lg: '16px' }, }) export const darkTheme = createTheme(themeContract, { color: { primary: '#3b82f6', secondary: '#9ca3af', text: '#f3f4f6', background: '#1f2937' }, space: { sm: '4px', md: '8px', lg: '16px' }, }) -
使用 Recipes 管理组件变体
// button.recipe.ts import { recipe } from '@vanilla-extract/recipes' export const button = recipe({ base: { padding: '8px 16px', borderRadius: '4px', border: 'none', cursor: 'pointer', }, variants: { intent: { primary: { backgroundColor: '#007bff', color: 'white' }, secondary: { backgroundColor: '#6c757d', color: 'white' }, }, size: { small: { fontSize: '12px', padding: '4px 8px' }, medium: { fontSize: '14px', padding: '8px 16px' }, large: { fontSize: '16px', padding: '12px 24px' }, }, }, defaultVariants: { intent: 'primary', size: 'medium', }, }) -
优先使用
fallbackVar——为 CSS 变量提供降级值,提高健壮性 -
使用
layer管理样式优先级——利用 CSS Cascade Layers 控制样式权重,避免!important
常见陷阱
- 在
.css.ts中引用运行时变量——所有值必须构建时可确定,不能引用props、state等运行时数据 - 过度使用
globalStyle——破坏局部作用域优势,增加样式冲突风险 - 忽略 TypeScript 性能——大型项目需监控 TS 编译时间,避免
.css.ts文件过于复杂 - 混用多个 CSS 方案——保持团队统一的样式策略,避免 vanilla-extract 与 CSS Modules 或 Tailwind 混用
@media查询排序——vanilla-extract 始终在文件末尾渲染@media查询,可能导致优先级问题,需用layer或@supports处理
技术局限与边界
已知限制
- 无运行时动态样式:所有样式必须构建时可静态分析,不能根据运行时状态(如
props.color)动态生成样式值。需要动态值时,必须使用@vanilla-extract/dynamic通过 CSS 变量中转 - Bundler 绑定:必须配置特定的 bundler 插件,不同构建工具的配置略有差异
@media查询排序:始终在文件末尾渲染@media查询,可能导致选择器优先级问题- HMR 不稳定:Vite 下热更新偶有样式不刷新的问题,需手动刷新页面
- SSR 中的
document is not defined:某些场景(如 Sprinkles)在 SSR 中可能报错 - Next.js 页面导航:CSS 在页面切换时偶有问题(App Router 中较稳定)
生态与维护
- 维护节奏放缓:根据 GitHub Discussion #1144,核心团队每周开会但由于外部职责带宽有限,"progress can be slow"
- 社区规模有限:相比 Tailwind(12M 下载),社区较小(~450K-1M 下载),遇到问题时可参考的资源有限
- 样式对设计师不友好:样式看起来像代码而非传统 CSS,设计团队难以直接参与
- 仅支持 TypeScript:
.css.ts文件必须是 TypeScript,纯 JavaScript 项目无法使用
不适用场景
- 高度动态样式:如用户自定义颜色主题(需 CSS 变量中转,体验不佳)
- 快速原型/MVP:学习曲线和配置成本高于 Tailwind 或 CSS Modules
- 小型简单项目:配置开销可能超过收益
- 非 TypeScript 项目:vanilla-extract 要求 TypeScript 环境
迁移注意事项
- 从 CSS Modules 迁移:主要是将
.module.css重写为.css.ts,类名映射从对象导入改为 TypeScript 导入 - 从运行时 CSS-in-JS 迁移:需要将所有运行时样式逻辑重构为构建时可确定的静态样式
- 从 Tailwind 迁移:差异较大,需要重新组织样式定义方式
学习资源
- 官方文档:https://vanilla-extract.style/documentation/getting-started
- GitHub 仓库:https://github.com/vanilla-extract-css/vanilla-extract
- GitHub Discussions:https://github.com/vanilla-extract-css/vanilla-extract/discussions (活跃的 Q&A 和想法讨论)
- Discord:官方 Discord 社区
- 示例项目:
- 教程与文章:
2026 年现状
最新版本
| 包名 | 版本 | 更新时间 |
|---|---|---|
@vanilla-extract/css | v1.21.1 | 持续维护中 |
@vanilla-extract/vite-plugin | v5.2.4 | 2026-07-06 |
@vanilla-extract/next-plugin | 持续更新 | 2026-04 |
@vanilla-extract/compiler | v0.7.0 | 2026-03 |
发展趋势
- 维护状态:根据 GitHub Discussion #1144,维护团队每周开会但由于外部职责带宽有限,项目仍然活跃但 "progress can be slow"。团队对 bug 修复持开放态度
- 社区规模:GitHub ~10,400+ Stars,npm 周下载量 ~450K-1M,约 118,000+ 仓库依赖
- 定位稳固:仍然是 2026 年 CSS-in-TypeScript 领域最成熟、最可靠的选择之一
- 无 v2 计划:核心 API 稳定,通过 minor 版本持续迭代
- 竞争格局:面临 Panda CSS(Chakra UI 团队,增长更快)和 Tailwind CSS v4(市场统治地位)的双重压力,但在类型安全设计系统领域仍具独特优势
社区活跃度
- GitHub 有 44 个 Open Issues、30 个 Open PRs
- Discussions 活跃,有 Q&A 和想法讨论
- 核心团队响应积极但节奏较慢
- 574+ 次发布记录显示持续的维护投入