Vanilla Extract
"vanilla-extract: zero-runtime CSS-in-TypeScript, sprinkles (utility classes), recipes (variants), themes, type-safe styles"
vanilla-extract is a zero-runtime CSS-in-TypeScript library. All styles are written in TypeScript files (`.css.ts`), evaluated at build time, and emitted as static CSS with locally scoped class names. There is no runtime style injection, which means zero performance cost in the browser. The TypeScript authoring surface gives full type safety, autocompletion, and the ability to share values (tokens, breakpoints, conditions) between styles and application code. The library provides three layers of abstraction: low-level `style()`, utility-class generation via Sprinkles, and variant-driven component APIs via Recipes. ## Key Points 1. **Co-locate style files with components.** Place `component.css.ts` next to `Component.tsx` for easy navigation and clear ownership. 2. **Use theme contracts for all shared values.** Never hardcode colors or spacing in individual style files; always reference `vars` so themes can be swapped. 3. **Prefer recipes for component variants.** Recipes produce a clean API surface (`intent`, `size`) and compose better than manual conditional class concatenation. 4. **Use sprinkles for layout primitives.** Reserve sprinkles for spacing, display, and alignment; keep visual styling (colors, shadows, borders) in `style()` or recipes. 5. **Export types alongside styles.** Export `RecipeVariants` and `Sprinkles` types so consuming components get full autocompletion and type checking. 6. **Keep style files pure.** Style files (`.css.ts`) should only contain style definitions and token references. Do not import React or runtime dependencies into them. 1. **Importing `.css.ts` files in non-component code.** Style files are evaluated at build time. Importing them dynamically or conditionally causes bundler errors or missing styles. 2. **Overusing `globalStyle`.** Global styles bypass scoping and can cause conflicts. Use them only for CSS resets or styling third-party markup you cannot add classes to. 3. **Duplicating tokens across files.** Define tokens once in a theme contract. Scattering raw values across style files leads to inconsistency and makes theming impossible. 4. **Creating deeply nested style compositions.** Flat, composable styles are easier to read and override. Avoid `styleVariants` with dozens of entries when a recipe would be clearer. 5. **Mixing vanilla-extract with runtime CSS-in-JS.** Combining it with styled-components or Emotion negates the zero-runtime benefit and doubles the CSS pipeline complexity. ## Quick Example ```bash npm install @vanilla-extract/css @vanilla-extract/recipes @vanilla-extract/sprinkles # Vite plugin npm install -D @vanilla-extract/vite-plugin ```
skilldb get css-styling-services-skills/Vanilla ExtractFull skill: 353 linesvanilla-extract
Core Philosophy
vanilla-extract is a zero-runtime CSS-in-TypeScript library. All styles are written in TypeScript files (.css.ts), evaluated at build time, and emitted as static CSS with locally scoped class names. There is no runtime style injection, which means zero performance cost in the browser. The TypeScript authoring surface gives full type safety, autocompletion, and the ability to share values (tokens, breakpoints, conditions) between styles and application code. The library provides three layers of abstraction: low-level style(), utility-class generation via Sprinkles, and variant-driven component APIs via Recipes.
Setup
Installation
npm install @vanilla-extract/css @vanilla-extract/recipes @vanilla-extract/sprinkles
# Vite plugin
npm install -D @vanilla-extract/vite-plugin
Vite Configuration
// vite.config.ts
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
});
Next.js Configuration
// next.config.ts
import { createVanillaExtractPlugin } from "@vanilla-extract/next-plugin";
const withVanillaExtract = createVanillaExtractPlugin();
export default withVanillaExtract({
// other Next.js config
});
Key Techniques
Basic Style Definitions
// card.css.ts
import { style, globalStyle } from "@vanilla-extract/css";
import { vars } from "./theme.css";
export const card = style({
backgroundColor: vars.color.surface,
borderRadius: vars.radius.lg,
padding: vars.space[6],
boxShadow: vars.shadow.sm,
transition: "box-shadow 0.2s ease, transform 0.2s ease",
":hover": {
boxShadow: vars.shadow.md,
transform: "translateY(-2px)",
},
":focus-within": {
outline: `2px solid ${vars.color.brand}`,
outlineOffset: "2px",
},
});
export const cardTitle = style({
fontSize: vars.fontSize.xl,
fontWeight: vars.fontWeight.semibold,
color: vars.color.textPrimary,
marginBottom: vars.space[2],
});
export const cardBody = style({
color: vars.color.textSecondary,
lineHeight: 1.6,
});
// Global style scoped to a parent
globalStyle(`${card} a`, {
color: vars.color.brand,
textDecoration: "underline",
});
Theme Contracts and Tokens
// theme.css.ts
import {
createTheme,
createThemeContract,
style,
} from "@vanilla-extract/css";
export const vars = createThemeContract({
color: {
brand: null,
brandDark: null,
surface: null,
textPrimary: null,
textSecondary: null,
border: null,
},
space: {
1: null, 2: null, 3: null, 4: null, 6: null, 8: null,
},
radius: { sm: null, md: null, lg: null, full: null },
fontSize: { sm: null, base: null, lg: null, xl: null, "2xl": null },
fontWeight: { normal: null, medium: null, semibold: null, bold: null },
shadow: { sm: null, md: null, lg: null },
});
export const lightTheme = createTheme(vars, {
color: {
brand: "#3b82f6",
brandDark: "#1d4ed8",
surface: "#ffffff",
textPrimary: "#111827",
textSecondary: "#6b7280",
border: "#e5e7eb",
},
space: {
1: "0.25rem", 2: "0.5rem", 3: "0.75rem",
4: "1rem", 6: "1.5rem", 8: "2rem",
},
radius: { sm: "0.25rem", md: "0.5rem", lg: "0.75rem", full: "9999px" },
fontSize: {
sm: "0.875rem", base: "1rem", lg: "1.125rem",
xl: "1.25rem", "2xl": "1.5rem",
},
fontWeight: { normal: "400", medium: "500", semibold: "600", bold: "700" },
shadow: {
sm: "0 1px 2px rgba(0,0,0,0.05)",
md: "0 4px 6px rgba(0,0,0,0.07)",
lg: "0 10px 15px rgba(0,0,0,0.1)",
},
});
export const darkTheme = createTheme(vars, {
color: {
brand: "#60a5fa",
brandDark: "#3b82f6",
surface: "#1f2937",
textPrimary: "#f9fafb",
textSecondary: "#9ca3af",
border: "#374151",
},
space: {
1: "0.25rem", 2: "0.5rem", 3: "0.75rem",
4: "1rem", 6: "1.5rem", 8: "2rem",
},
radius: { sm: "0.25rem", md: "0.5rem", lg: "0.75rem", full: "9999px" },
fontSize: {
sm: "0.875rem", base: "1rem", lg: "1.125rem",
xl: "1.25rem", "2xl": "1.5rem",
},
fontWeight: { normal: "400", medium: "500", semibold: "600", bold: "700" },
shadow: {
sm: "0 1px 2px rgba(0,0,0,0.3)",
md: "0 4px 6px rgba(0,0,0,0.4)",
lg: "0 10px 15px rgba(0,0,0,0.5)",
},
});
Recipes for Variant-Driven Components
// button.css.ts
import { recipe, type RecipeVariants } from "@vanilla-extract/recipes";
import { vars } from "./theme.css";
export const button = recipe({
base: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontWeight: vars.fontWeight.medium,
borderRadius: vars.radius.md,
transition: "background-color 0.15s, box-shadow 0.15s",
cursor: "pointer",
border: "none",
":focus-visible": {
outline: `2px solid ${vars.color.brand}`,
outlineOffset: "2px",
},
":disabled": {
opacity: 0.5,
cursor: "not-allowed",
},
},
variants: {
intent: {
primary: {
backgroundColor: vars.color.brand,
color: "#fff",
":hover": { backgroundColor: vars.color.brandDark },
},
secondary: {
backgroundColor: "transparent",
color: vars.color.textPrimary,
border: `1px solid ${vars.color.border}`,
":hover": { backgroundColor: vars.color.surface },
},
ghost: {
backgroundColor: "transparent",
color: vars.color.textSecondary,
":hover": { color: vars.color.textPrimary },
},
},
size: {
sm: { fontSize: vars.fontSize.sm, padding: `${vars.space[1]} ${vars.space[3]}` },
md: { fontSize: vars.fontSize.base, padding: `${vars.space[2]} ${vars.space[4]}` },
lg: { fontSize: vars.fontSize.lg, padding: `${vars.space[3]} ${vars.space[6]}` },
},
},
defaultVariants: {
intent: "primary",
size: "md",
},
});
export type ButtonVariants = RecipeVariants<typeof button>;
Using Recipes in Components
// Button.tsx
import { button, type ButtonVariants } from "./button.css";
type ButtonProps = ButtonVariants &
React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
intent,
size,
className,
children,
...props
}: ButtonProps) {
return (
<button className={button({ intent, size })} {...props}>
{children}
</button>
);
}
Sprinkles for Utility Classes
// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";
import { vars } from "./theme.css";
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", "grid", "inline-flex"],
flexDirection: ["row", "column"],
alignItems: ["stretch", "center", "flex-start", "flex-end"],
justifyContent: ["center", "space-between", "flex-start", "flex-end"],
gap: vars.space,
padding: vars.space,
paddingX: vars.space,
paddingY: vars.space,
fontSize: vars.fontSize,
},
shorthands: {
px: ["paddingX"],
py: ["paddingY"],
p: ["padding"],
},
});
const colorProperties = defineProperties({
conditions: {
light: {},
dark: { "@media": "(prefers-color-scheme: dark)" },
},
defaultCondition: "light",
properties: {
color: vars.color,
backgroundColor: vars.color,
},
});
export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
export type Sprinkles = Parameters<typeof sprinkles>[0];
Using Sprinkles in a Layout Component
// Stack.tsx
import { sprinkles, type Sprinkles } from "./sprinkles.css";
interface StackProps extends Pick<Sprinkles, "gap" | "alignItems"> {
children: React.ReactNode;
as?: React.ElementType;
}
export function Stack({
gap = 4,
alignItems = "stretch",
as: Tag = "div",
children,
}: StackProps) {
return (
<Tag
className={sprinkles({
display: "flex",
flexDirection: "column",
gap,
alignItems,
})}
>
{children}
</Tag>
);
}
Best Practices
- Co-locate style files with components. Place
component.css.tsnext toComponent.tsxfor easy navigation and clear ownership. - Use theme contracts for all shared values. Never hardcode colors or spacing in individual style files; always reference
varsso themes can be swapped. - Prefer recipes for component variants. Recipes produce a clean API surface (
intent,size) and compose better than manual conditional class concatenation. - Use sprinkles for layout primitives. Reserve sprinkles for spacing, display, and alignment; keep visual styling (colors, shadows, borders) in
style()or recipes. - Export types alongside styles. Export
RecipeVariantsandSprinklestypes so consuming components get full autocompletion and type checking. - Keep style files pure. Style files (
.css.ts) should only contain style definitions and token references. Do not import React or runtime dependencies into them.
Anti-Patterns
- Importing
.css.tsfiles in non-component code. Style files are evaluated at build time. Importing them dynamically or conditionally causes bundler errors or missing styles. - Overusing
globalStyle. Global styles bypass scoping and can cause conflicts. Use them only for CSS resets or styling third-party markup you cannot add classes to. - Duplicating tokens across files. Define tokens once in a theme contract. Scattering raw values across style files leads to inconsistency and makes theming impossible.
- Creating deeply nested style compositions. Flat, composable styles are easier to read and override. Avoid
styleVariantswith dozens of entries when a recipe would be clearer. - Mixing vanilla-extract with runtime CSS-in-JS. Combining it with styled-components or Emotion negates the zero-runtime benefit and doubles the CSS pipeline complexity.
Install this skill directly: skilldb add css-styling-services-skills
Related Skills
CSS Modules
"CSS Modules: scoped CSS, composition, global styles, Next.js integration, TypeScript declarations, naming conventions"
Lightning CSS
"Lightning CSS: ultra-fast CSS transformer, bundler, and minifier written in Rust, with modern syntax lowering, vendor prefixing, and CSS modules"
Panda CSS
"Panda CSS: build-time CSS-in-JS, atomic CSS, recipes, patterns, tokens, conditions, RSC compatible, type-safe styles"
Stitches
"Stitches: near-zero runtime CSS-in-JS, variants API, theme tokens, responsive styles, SSR support, polymorphic components"
Styled Components
"styled-components: CSS-in-JS, tagged template literals, theming, dynamic styles, SSR, global styles, extending"
Tailwind CSS
"Tailwind CSS: utility-first CSS, responsive design, dark mode, custom theme, plugins, @apply, JIT, content configuration, arbitrary values"