vanilla-extract logo

vanilla-extract

使用 TypeScript 编写并在构建期输出静态 CSS 的样式方案

精选工具零运行时TypeScriptCSS变量设计系统

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 扩展)等能力,同时保持零运行时的性能优势。

基本信息

快速上手

安装

以 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 文件
  1. 编写样式:在 .css.ts 文件中用 TypeScript 编写样式定义
  2. 构建处理:Bundler 插件拦截 .css.ts 文件,在构建时执行 TypeScript 求值
  3. CSS 提取:所有样式被提取为标准 CSS 文件
  4. 类名映射.css.ts 文件导出局部作用域的类名字符串
  5. 消费样式:在组件中导入类名字符串并应用到元素

关键约束:所有样式必须在构建时可静态分析,不支持运行时动态生成样式。.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✅ 支持
Gatsbygatsby-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 SprinklesSprinkles 的扩展,支持响应式和条件变体

关键依赖

  • vanilla-extract 本身无重依赖,核心包极轻量
  • 构建时依赖 TypeScript 编译器评估 .css.ts 文件
  • Sprinkles 依赖核心包的 CSS 变量系统

适用场景

  • 大型设计系统:vanilla-extract 的类型安全主题合约和设计 Token 管理是构建大型设计系统的理想选择,确保跨团队的一致性
  • 组件库开发:局部作用域类名和零运行时特性使其成为开源组件库的优选,不影响使用者的运行时性能
  • 企业级应用:TypeScript 类型安全、主题合约和 CSS 变量支持适合需要严格规范的企业级项目
  • 设计 Token 驱动的项目createThemeContract 提供的类型安全 Token 合约,确保设计系统值的正确传递和使用
  • React Server Components 项目:零运行时特性天然兼容 RSC,不会引入客户端样式计算
  • 多主题应用:通过 createThemeassignVars 实现类型安全的多主题切换(如 light/dark/brand 主题)
  • 跨框架项目:框架无关的设计使其可在 React、Vue、Svelte 混合使用的 monorepo 中统一样式方案

开发与工程化

开发流程

  1. 初始化项目:安装 vanilla-extract 和对应的 bundler 插件
  2. 创建设计 Token:在 .css.ts 文件中定义颜色、间距、字体等基础 Token
  3. 创建主题合约:使用 createThemeContract 定义主题结构
  4. 编写组件样式:在 .css.ts 文件中为组件编写样式
  5. 在组件中消费:导入类名字符串应用到 JSX 元素
  6. 构建部署: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 求值有一定开销,但增量编译表现良好

性能优化建议

  1. 使用 Sprinkles 构建原子化基础层——减少 CSS 文件体积,提高类名复用率
  2. 合理使用 globalStyle——全局样式过多会增加 CSS 文件体积
  3. 避免过深的样式嵌套——复杂选择器可能影响浏览器渲染性能
  4. 监控 TypeScript 编译时间——大型项目中 .css.ts 文件的 TS 编译可能成为瓶颈

安全注意事项

  • vanilla-extract 生成的 CSS 类名是哈希值,不存在 XSS 注入风险
  • CSS 变量值是静态的,不存在运行时注入风险
  • 使用 globalStyle 时注意避免样式污染

已知性能瓶颈

  • TypeScript 编译:大量 .css.ts 文件的 TS 类型检查可能明显变慢
  • HMR 延迟:Vite 下的热更新偶有样式不刷新的问题(社区已知)
  • 大型主题合约:过于复杂的 createThemeContract 可能增加构建时间

技术对比

维度vanilla-extractPanda CSSPigment CSSKuma UITailwind CSSCSS 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 兼容性上全面领先

最佳实践

推荐模式

  1. 使用 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)
  2. 使用 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' },
    })
  3. 使用 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',
      },
    })
  4. 优先使用 fallbackVar——为 CSS 变量提供降级值,提高健壮性

  5. 使用 layer 管理样式优先级——利用 CSS Cascade Layers 控制样式权重,避免 !important

常见陷阱

  1. .css.ts 中引用运行时变量——所有值必须构建时可确定,不能引用 propsstate 等运行时数据
  2. 过度使用 globalStyle——破坏局部作用域优势,增加样式冲突风险
  3. 忽略 TypeScript 性能——大型项目需监控 TS 编译时间,避免 .css.ts 文件过于复杂
  4. 混用多个 CSS 方案——保持团队统一的样式策略,避免 vanilla-extract 与 CSS Modules 或 Tailwind 混用
  5. @media 查询排序——vanilla-extract 始终在文件末尾渲染 @media 查询,可能导致优先级问题,需用 layer@supports 处理

技术局限与边界

已知限制

  1. 无运行时动态样式:所有样式必须构建时可静态分析,不能根据运行时状态(如 props.color)动态生成样式值。需要动态值时,必须使用 @vanilla-extract/dynamic 通过 CSS 变量中转
  2. Bundler 绑定:必须配置特定的 bundler 插件,不同构建工具的配置略有差异
  3. @media 查询排序:始终在文件末尾渲染 @media 查询,可能导致选择器优先级问题
  4. HMR 不稳定:Vite 下热更新偶有样式不刷新的问题,需手动刷新页面
  5. SSR 中的 document is not defined:某些场景(如 Sprinkles)在 SSR 中可能报错
  6. Next.js 页面导航:CSS 在页面切换时偶有问题(App Router 中较稳定)

生态与维护

  1. 维护节奏放缓:根据 GitHub Discussion #1144,核心团队每周开会但由于外部职责带宽有限,"progress can be slow"
  2. 社区规模有限:相比 Tailwind(12M 下载),社区较小(~450K-1M 下载),遇到问题时可参考的资源有限
  3. 样式对设计师不友好:样式看起来像代码而非传统 CSS,设计团队难以直接参与
  4. 仅支持 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 迁移:差异较大,需要重新组织样式定义方式

学习资源

2026 年现状

最新版本

包名版本更新时间
@vanilla-extract/cssv1.21.1持续维护中
@vanilla-extract/vite-pluginv5.2.42026-07-06
@vanilla-extract/next-plugin持续更新2026-04
@vanilla-extract/compilerv0.7.02026-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+ 次发布记录显示持续的维护投入