CSS Modules are safe but untyped. CSS-in-JS is typed but ships runtime. Vanilla Extract gives you both — type-safe styles that compile to plain CSS at build time.
What Is Vanilla Extract?
Vanilla Extract is a zero-runtime CSS-in-TypeScript library. Write your styles in .css.ts files — they compile to scoped CSS at build time. No JavaScript ships to the browser for styling.
Quick Start
npm install @vanilla-extract/css
// styles.css.ts
import { style, globalStyle } from '@vanilla-extract/css';
export const container = style({
maxWidth: '1200px',
margin: '0 auto',
padding: '0 16px',
});
export const heading = style({
fontSize: '2rem',
fontWeight: 700,
color: '#1a1a2e',
marginBottom: '1rem',
':hover': {
color: '#16213e',
},
'@media': {
'(max-width: 768px)': {
fontSize: '1.5rem',
},
},
});
// Page.tsx
import { container, heading } from './styles.css';
export function Page() {
return (
<div className={container}>
<h1 className={heading}>Hello World</h1>
</div>
);
}
Sprinkles (Utility Classes)
Like Tailwind, but type-safe:
// sprinkles.css.ts
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
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'],
gap: { 0: '0', 1: '4px', 2: '8px', 3: '12px', 4: '16px' },
padding: { 0: '0', 1: '4px', 2: '8px', 4: '16px', 8: '32px' },
},
});
export const sprinkles = createSprinkles(responsiveProperties);
<div className={sprinkles({
display: 'flex',
flexDirection: { mobile: 'column', desktop: 'row' },
gap: 4,
padding: { mobile: 2, desktop: 8 },
})}>
Recipes (Component Variants)
import { recipe } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
borderRadius: '6px',
fontWeight: 600,
border: 'none',
cursor: 'pointer',
},
variants: {
color: {
primary: { background: '#3b82f6', color: 'white' },
secondary: { background: '#6b7280', color: 'white' },
},
size: {
small: { padding: '8px 16px', fontSize: '14px' },
large: { padding: '16px 32px', fontSize: '18px' },
},
},
defaultVariants: { color: 'primary', size: 'small' },
});
// Usage
<button className={button({ color: 'primary', size: 'large' })}>
Click me
</button>
Theme System
import { createTheme, createThemeContract } from '@vanilla-extract/css';
const themeContract = createThemeContract({
color: { brand: '', text: '', background: '' },
space: { small: '', medium: '', large: '' },
});
export const lightTheme = createTheme(themeContract, {
color: { brand: '#3b82f6', text: '#1a1a2e', background: '#ffffff' },
space: { small: '4px', medium: '16px', large: '32px' },
});
export const darkTheme = createTheme(themeContract, {
color: { brand: '#60a5fa', text: '#e5e7eb', background: '#1a1a2e' },
space: { small: '4px', medium: '16px', large: '32px' },
});
Framework Support
Works with Vite, webpack, Next.js, Remix, esbuild, and Parcel.
Get Started
- Documentation
- GitHub — 9K+ stars
- Playground
Building a styled web app? My Apify scrapers deliver data for your dashboards. Custom solutions: spinov001@gmail.com
Top comments (0)