DEV Community

Alex Spinov
Alex Spinov

Posted on

Vanilla Extract Has Free Zero-Runtime CSS — Here's How to Use It

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
Enter fullscreen mode Exit fullscreen mode
// 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',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode
// Page.tsx
import { container, heading } from './styles.css';

export function Page() {
  return (
    <div className={container}>
      <h1 className={heading}>Hello World</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
<div className={sprinkles({
  display: 'flex',
  flexDirection: { mobile: 'column', desktop: 'row' },
  gap: 4,
  padding: { mobile: 2, desktop: 8 },
})}>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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' },
});
Enter fullscreen mode Exit fullscreen mode

Framework Support

Works with Vite, webpack, Next.js, Remix, esbuild, and Parcel.

Get Started


Building a styled web app? My Apify scrapers deliver data for your dashboards. Custom solutions: spinov001@gmail.com

Top comments (0)