Vanilla Extract: Zero-Runtime CSS-in-TypeScript
A practical guide to Vanilla Extract — type-safe styles at build time, theming with CSS variables, recipes and sprinkles, and when to pick it over Tailwind or CSS Modules.
A practical guide to CSS architecture patterns — when to use Tailwind for rapid development, CSS Modules for component scoping, or CSS-in-JS for dynamic styling.
By codefunc
Every frontend project needs a CSS strategy. The wrong choice leads to specificity wars, bloated bundles, or unmaintainable stylesheets. In 2026, three approaches dominate: Tailwind for utility-first rapid development, CSS Modules for scoped component styles, and CSS-in-JS for dynamic runtime styling.
Bottom line: Use Tailwind for most projects — it ships less CSS, has excellent DX, and scales well. Use CSS Modules when you want traditional CSS with guaranteed scoping. Use CSS-in-JS (Styled Components, Emotion) when you need heavy runtime theming or are in a styled-components codebase. Avoid mixing multiple approaches in one project.
| Feature | Tailwind | CSS Modules | CSS-in-JS |
|---|---|---|---|
| Learning curve | Medium | Low | Medium |
| Bundle size | Small (purged) | Medium | Large |
| Runtime cost | None | None | Yes |
| Dynamic styles | Limited | No | Yes |
| Type safety | With plugins | No | Yes |
| Server components | Yes | Yes | Partial |
| Tooling required | PostCSS | CSS loader | Runtime library |
Tailwind provides utility classes that you compose directly in markup.
// Instead of writing CSS...
// .card { padding: 1rem; background: white; border-radius: 0.5rem; }
// You write utilities in markup
<div className="p-4 bg-white rounded-lg shadow-md">
<h2 className="text-xl font-bold text-gray-900">Card Title</h2>
<p className="mt-2 text-gray-600">Card content goes here.</p>
</div>
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
brand: '#10b981',
},
},
},
plugins: [],
};
/* src/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Extracting components:
// Don't repeat long class strings
// Extract to components instead
function Button({ children, variant = 'primary' }) {
const base = 'px-4 py-2 rounded-lg font-medium transition-colors';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
};
return (
<button className={`${base} ${variants[variant]}`}>
{children}
</button>
);
}
Using @apply (sparingly):
/* Only for truly repeated patterns */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700;
}
}
Responsive design:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Mobile: 1 column, Tablet: 2 columns, Desktop: 3 columns */}
</div>
Dark mode:
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Adapts to color scheme
</div>
sm:, md:, lg: prefixesdark: prefixCSS Modules scope class names to components automatically.
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
}
.primary {
background-color: #2563eb;
color: white;
}
.secondary {
background-color: #e5e7eb;
color: #111827;
}
// Button.tsx
import styles from './Button.module.css';
function Button({ children, variant = 'primary' }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
Generated HTML:
<button class="Button_button__x7f3k Button_primary__a2b4c">
Click me
</button>
Composing styles:
/* base.module.css */
.text {
font-family: system-ui;
line-height: 1.5;
}
/* Button.module.css */
.button {
composes: text from './base.module.css';
padding: 0.5rem 1rem;
}
Global styles:
/* When you need to target global classes */
.wrapper :global(.third-party-class) {
color: red;
}
With CSS variables:
.button {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}
CSS-in-JS writes styles in JavaScript, enabling dynamic styling.
import styled from 'styled-components';
const Button = styled.button<{ $primary?: boolean }>`
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
background-color: ${props => props.$primary ? '#2563eb' : '#e5e7eb'};
color: ${props => props.$primary ? 'white' : '#111827'};
&:hover {
opacity: 0.9;
}
`;
function App() {
return (
<>
<Button $primary>Primary</Button>
<Button>Secondary</Button>
</>
);
}
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = (primary: boolean) => css`
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: ${primary ? '#2563eb' : '#e5e7eb'};
color: ${primary ? 'white' : '#111827'};
`;
function Button({ primary, children }) {
return <button css={buttonStyle(primary)}>{children}</button>;
}
Theming:
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#2563eb',
secondary: '#e5e7eb',
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
};
const Button = styled.button`
background-color: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.md};
`;
function App() {
return (
<ThemeProvider theme={theme}>
<Button>Themed Button</Button>
</ThemeProvider>
);
}
Extending styles:
const Button = styled.button`
padding: 0.5rem 1rem;
border-radius: 0.5rem;
`;
const PrimaryButton = styled(Button)`
background-color: #2563eb;
color: white;
`;
Tailwind:
function Card({ title, highlighted }) {
return (
<div className={`p-4 rounded-lg shadow-md ${
highlighted ? 'bg-yellow-50 border-yellow-200' : 'bg-white'
}`}>
<h2 className="text-xl font-bold">{title}</h2>
</div>
);
}
CSS Modules:
/* Card.module.css */
.card {
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
background-color: white;
}
.highlighted {
background-color: #fefce8;
border-color: #fef08a;
}
.title {
font-size: 1.25rem;
font-weight: 700;
}
import styles from './Card.module.css';
function Card({ title, highlighted }) {
return (
<div className={`${styles.card} ${highlighted ? styles.highlighted : ''}`}>
<h2 className={styles.title}>{title}</h2>
</div>
);
}
Styled Components:
const CardWrapper = styled.div<{ $highlighted?: boolean }>`
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
background-color: ${p => p.$highlighted ? '#fefce8' : 'white'};
border-color: ${p => p.$highlighted ? '#fef08a' : 'transparent'};
`;
const Title = styled.h2`
font-size: 1.25rem;
font-weight: 700;
`;
function Card({ title, highlighted }) {
return (
<CardWrapper $highlighted={highlighted}>
<Title>{title}</Title>
</CardWrapper>
);
}
For a medium app (~50 components):
| Approach | CSS size | JS overhead |
|---|---|---|
| Tailwind | ~10KB | 0 |
| CSS Modules | ~30KB | 0 |
| Styled Components | ~20KB | ~15KB runtime |
Use Tailwind for utilities, CSS Modules for complex components:
import styles from './ComplexAnimation.module.css';
function Component() {
return (
<div className={`p-4 ${styles.animatedGradient}`}>
Content with both Tailwind spacing and custom animation
</div>
);
}
Define tokens once, use in any approach:
:root {
--color-primary: #2563eb;
--spacing-md: 1rem;
--radius-lg: 0.5rem;
}
Works with Tailwind (theme.extend), CSS Modules (var(--color-primary)), and CSS-in-JS.
For optimizing your CSS output, use the CSS Minifier. Calculate fluid typography with CSS Clamp Calculator. Convert colors between formats with Color Converter. Generate border radius values with Border Radius Generator.
Need heavy runtime theming?
├── Yes → CSS-in-JS (Styled Components)
└── No → Want utility-first?
├── Yes → Tailwind
└── No → CSS Modules
Tailwind is the default choice for new projects — it ships small bundles, enforces consistency, and scales well. CSS Modules work best when your team prefers traditional CSS or needs full Server Component support. CSS-in-JS suits apps with complex theming or existing styled-components codebases. Pick one approach and stick with it. Mixing approaches creates maintenance burden.
A practical guide to Vanilla Extract — type-safe styles at build time, theming with CSS variables, recipes and sprinkles, and when to pick it over Tailwind or CSS Modules.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
Why compact GraphQL queries hurt reviews, how to format selection sets consistently, and a browser workflow before you paste into Apollo or GraphiQL.
Find a developer tool