| name | vanilla-extract |
| description | Implements zero-runtime CSS using vanilla-extract with type-safe styles, themes, recipes, and sprinkles. Use when wanting type-safe CSS, static extraction at build time, or building design system utilities. |
vanilla-extract
Zero-runtime CSS-in-TypeScript with static extraction at build time.
Quick Start
Install:
npm install @vanilla-extract/css
# Framework integrations
npm install @vanilla-extract/vite-plugin # Vite
npm install @vanilla-extract/next-plugin # Next.js
Configure (Vite):
// vite.config.ts
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [vanillaExtractPlugin()],
});
Create styles:
// button.css.ts
import { style } from '@vanilla-extract/css';
export const button = style({
padding: '12px 24px',
border: 'none',
borderRadius: 8,
fontSize: 16,
cursor: 'pointer',
backgroundColor: '#3b82f6',
color: 'white',
':hover': {
backgroundColor: '#2563eb',
},
});
Use in component:
// Button.tsx
import { button } from './button.css';
export function Button({ children }: { children: React.ReactNode }) {
return <button className={button}>{children}</button>;
}
Style API
Basic Styles
import { style } from '@vanilla-extract/css';
export const container = style({
maxWidth: 1200,
margin: '0 auto',
padding: 16,
});
// Numbers become pixels (except unitless properties)
export const box = style({
padding: 16, // 16px
margin: 8, // 8px
opacity: 0.5, // unitless
flexGrow: 1, // unitless
lineHeight: 1.5, // unitless
});
Pseudo-Selectors
export const link = style({
color: '#3b82f6',
textDecoration: 'none',
':hover': {
textDecoration: 'underline',
},
':focus-visible': {
outline: '2px solid #3b82f6',
outlineOffset: 2,
},
'::before': {
content: '">"',
marginRight: 4,
},
});
Complex Selectors
export const card = style({
padding: 16,
selectors: {
// Target self with conditions
'&:first-child': {
marginTop: 0,
},
// Adjacent sibling
'& + &': {
marginTop: 16,
},
// Parent hover (& must appear in selector)
'.dark-mode &': {
backgroundColor: '#1f2937',
},
// Direct child - use globalStyle instead
// '& > div': { } // Invalid!
},
});
Media Queries
export const responsiveBox = style({
padding: 16,
'@media': {
'(min-width: 768px)': {
padding: 24,
},
'(min-width: 1024px)': {
padding: 32,
},
'(prefers-color-scheme: dark)': {
backgroundColor: '#1f2937',
},
},
});
Container Queries
export const containerParent = style({
containerType: 'inline-size',
});
export const responsiveChild = style({
padding: 16,
'@container': {
'(min-width: 400px)': {
padding: 24,
},
},
});
CSS Variables
createVar
import { style, createVar } from '@vanilla-extract/css';
const accentColor = createVar();
const spacing = createVar();
export const container = style({
vars: {
[accentColor]: '#3b82f6',
[spacing]: '16px',
},
padding: spacing,
borderColor: accentColor,
});
export const altContainer = style({
vars: {
[accentColor]: '#10b981', // Override
},
});
Fallback Values
import { fallbackVar } from '@vanilla-extract/css';
export const box = style({
color: fallbackVar(accentColor, 'blue'),
});
Style Variants
import { styleVariants } from '@vanilla-extract/css';
// Simple variants
export const color = styleVariants({
primary: { backgroundColor: '#3b82f6', color: 'white' },
secondary: { backgroundColor: '#e5e7eb', color: '#1f2937' },
danger: { backgroundColor: '#ef4444', color: 'white' },
});
// With composition
const base = style({
padding: '12px 24px',
borderRadius: 8,
border: 'none',
});
export const button = styleVariants({
primary: [base, { backgroundColor: '#3b82f6', color: 'white' }],
secondary: [base, { backgroundColor: '#e5e7eb', color: '#1f2937' }],
});
// Usage
<button className={button.primary}>Primary</button>
<button className={color['secondary']}>Secondary</button>
Global Styles
import { globalStyle, style } from '@vanilla-extract/css';
// Global reset
globalStyle('*, *::before, *::after', {
boxSizing: 'border-box',
});
globalStyle('body', {
margin: 0,
fontFamily: 'system-ui, sans-serif',
});
// Reference scoped classes
const card = style({ padding: 16 });
globalStyle(`${card} > h2`, {
margin: 0,
fontSize: 24,
});
globalStyle(`${card} p`, {
color: '#6b7280',
});
Theming
Create Theme
// theme.css.ts
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
background: '#ffffff',
text: '#1f2937',
},
spacing: {
sm: '8px',
md: '16px',
lg: '24px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '16px',
},
});
Use Theme Variables
// button.css.ts
import { style } from '@vanilla-extract/css';
import { vars } from './theme.css';
export const button = style({
padding: vars.spacing.md,
borderRadius: vars.borderRadius.md,
backgroundColor: vars.colors.primary,
color: '#fff',
});
Multiple Themes
import { createTheme, createThemeContract } from '@vanilla-extract/css';
// Define contract (structure only)
const themeContract = createThemeContract({
colors: {
background: null,
text: null,
primary: null,
},
});
// Light theme
export const lightTheme = createTheme(themeContract, {
colors: {
background: '#ffffff',
text: '#1f2937',
primary: '#3b82f6',
},
});
// Dark theme
export const darkTheme = createTheme(themeContract, {
colors: {
background: '#1f2937',
text: '#f9fafb',
primary: '#60a5fa',
},
});
export { themeContract as vars };
Apply theme:
function App() {
const [isDark, setIsDark] = useState(false);
return (
<div className={isDark ? darkTheme : lightTheme}>
<button onClick={() => setIsDark(!isDark)}>Toggle</button>
</div>
);
}
Recipes
Multi-variant component styles.
Install:
npm install @vanilla-extract/recipes
// button.css.ts
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontWeight: 600,
},
variants: {
color: {
primary: {
backgroundColor: '#3b82f6',
color: 'white',
},
secondary: {
backgroundColor: '#e5e7eb',
color: '#1f2937',
},
danger: {
backgroundColor: '#ef4444',
color: 'white',
},
},
size: {
sm: { padding: '8px 16px', fontSize: 14 },
md: { padding: '12px 24px', fontSize: 16 },
lg: { padding: '16px 32px', fontSize: 18 },
},
},
compoundVariants: [
{
variants: { color: 'primary', size: 'lg' },
style: {
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.4)',
},
},
],
defaultVariants: {
color: 'primary',
size: 'md',
},
});
// Type extraction
export type ButtonVariants = RecipeVariants<typeof button>;
Usage:
import { button, ButtonVariants } from './button.css';
interface ButtonProps extends ButtonVariants {
children: React.ReactNode;
}
export function Button({ color, size, children }: ButtonProps) {
return (
<button className={button({ color, size })}>
{children}
</button>
);
}
// Usage
<Button color="primary" size="lg">Click me</Button>
<Button color="danger">Delete</Button>
Sprinkles
Build atomic CSS utilities.
Install:
npm install @vanilla-extract/sprinkles
// sprinkles.css.ts
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
const space = {
none: '0',
sm: '4px',
md: '8px',
lg: '16px',
xl: '24px',
};
const colors = {
primary: '#3b82f6',
secondary: '#6b7280',
white: '#ffffff',
black: '#000000',
};
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': '(min-width: 768px)' },
desktop: { '@media': '(min-width: 1024px)' },
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block', 'grid'],
flexDirection: ['row', 'column'],
alignItems: ['stretch', 'center', 'flex-start', 'flex-end'],
justifyContent: ['stretch', 'center', 'flex-start', 'flex-end', 'space-between'],
gap: space,
padding: space,
paddingTop: space,
paddingBottom: space,
paddingLeft: space,
paddingRight: space,
margin: space,
},
shorthands: {
p: ['padding'],
px: ['paddingLeft', 'paddingRight'],
py: ['paddingTop', 'paddingBottom'],
m: ['margin'],
},
});
const colorProperties = defineProperties({
properties: {
color: colors,
backgroundColor: colors,
},
});
export const sprinkles = createSprinkles(
responsiveProperties,
colorProperties
);
export type Sprinkles = Parameters<typeof sprinkles>[0];
Usage:
import { sprinkles } from './sprinkles.css';
function Box() {
return (
<div className={sprinkles({
display: 'flex',
gap: 'lg',
p: { mobile: 'md', desktop: 'xl' },
backgroundColor: 'white',
})}>
Content
</div>
);
}
Framework Setup
Next.js
// next.config.js
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
const withVanillaExtract = createVanillaExtractPlugin();
module.exports = withVanillaExtract({
// Next.js config
});
Vite
// vite.config.ts
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
export default defineConfig({
plugins: [vanillaExtractPlugin()],
});
Best Practices
- Use
.css.tsextension - Required for processing - Colocate styles - Keep near components
- Export vars - Share theme variables
- Use recipes for variants - Type-safe component APIs
- Sprinkles for utilities - Build design system primitives
Reference Files
- references/recipes.md - Recipe patterns
- references/sprinkles.md - Atomic CSS utilities