/ astro-integrations / How to Integrate Vanilla Extract with Astro: Complete Guide
astro-integrations 6 min read

How to Integrate Vanilla Extract with Astro: Complete Guide

Step-by-step guide to integrating Vanilla Extract with your Astro website.

Vanilla Extract is a CSS-in-TypeScript solution that generates static CSS at build time. You write your styles in TypeScript files, get full type safety and autocompletion, and the output is plain CSS with zero runtime cost. No JavaScript ships to the browser for styling. No CSS-in-JS runtime. Just static stylesheets.

For Astro projects, this means you get the developer experience of CSS-in-JS (variables, theming, type-safe tokens) with the performance of traditional CSS. It works through a Vite plugin, which Astro uses as its build tool.

Prerequisites

  • Node.js 18+
  • An Astro project (npm create astro@latest)
  • TypeScript configured (Astro includes this by default)

Installation

npm install @vanilla-extract/css @vanilla-extract/vite-plugin

For additional features:

# Sprinkles (utility-class generator)
npm install @vanilla-extract/sprinkles

# Recipes (variant API)
npm install @vanilla-extract/recipes

Configuration

Add the Vanilla Extract Vite plugin to your astro.config.mjs:

import { defineConfig } from "astro/config";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";

export default defineConfig({
  vite: {
    plugins: [vanillaExtractPlugin()],
  },
});

That is the entire setup. No additional configuration needed.

Creating Styles

Vanilla Extract styles live in .css.ts files. This naming convention tells the plugin to process them:

// src/styles/card.css.ts
import { style } from "@vanilla-extract/css";

export const card = style({
  backgroundColor: "white",
  borderRadius: "12px",
  padding: "24px",
  boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
  transition: "box-shadow 0.2s ease",
  ":hover": {
    boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
  },
});

export const cardTitle = style({
  fontSize: "1.25rem",
  fontWeight: 700,
  color: "#111827",
  marginBottom: "8px",
});

export const cardDescription = style({
  fontSize: "1rem",
  color: "#6B7280",
  lineHeight: 1.6,
});

Use them in your Astro components:

---
import { card, cardTitle, cardDescription } from "../styles/card.css";

export interface Props {
  title: string;
  description: string;
}

const { title, description } = Astro.props;
---

<div class={card}>
  <h3 class={cardTitle}>{title}</h3>
  <p class={cardDescription}>{description}</p>
</div>

The classes are hashed automatically, so you never have naming collisions. card becomes something like card_abc123 in the output.

Theme Tokens

Define a consistent design system with theme tokens:

// src/styles/theme.css.ts
import { createTheme, createThemeContract } from "@vanilla-extract/css";

export const vars = createThemeContract({
  color: {
    primary: null,
    secondary: null,
    text: null,
    textMuted: null,
    background: null,
    surface: null,
    border: null,
  },
  font: {
    body: null,
    heading: null,
    mono: null,
  },
  space: {
    xs: null,
    sm: null,
    md: null,
    lg: null,
    xl: null,
  },
  radius: {
    sm: null,
    md: null,
    lg: null,
    full: null,
  },
});

export const lightTheme = createTheme(vars, {
  color: {
    primary: "#0ea5e9",
    secondary: "#8b5cf6",
    text: "#111827",
    textMuted: "#6b7280",
    background: "#ffffff",
    surface: "#f9fafb",
    border: "#e5e7eb",
  },
  font: {
    body: "Inter, sans-serif",
    heading: "Inter, sans-serif",
    mono: "JetBrains Mono, monospace",
  },
  space: {
    xs: "4px",
    sm: "8px",
    md: "16px",
    lg: "32px",
    xl: "64px",
  },
  radius: {
    sm: "4px",
    md: "8px",
    lg: "16px",
    full: "9999px",
  },
});

export const darkTheme = createTheme(vars, {
  color: {
    primary: "#38bdf8",
    secondary: "#a78bfa",
    text: "#f9fafb",
    textMuted: "#9ca3af",
    background: "#111827",
    surface: "#1f2937",
    border: "#374151",
  },
  font: {
    body: "Inter, sans-serif",
    heading: "Inter, sans-serif",
    mono: "JetBrains Mono, monospace",
  },
  space: {
    xs: "4px",
    sm: "8px",
    md: "16px",
    lg: "32px",
    xl: "64px",
  },
  radius: {
    sm: "4px",
    md: "8px",
    lg: "16px",
    full: "9999px",
  },
});

Use the theme tokens in your styles:

// src/styles/button.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "./theme.css";

export const button = style({
  backgroundColor: vars.color.primary,
  color: "white",
  padding: vars.space.sm + " " + vars.space.md,
  borderRadius: vars.radius.md,
  fontFamily: vars.font.body,
  fontWeight: 600,
  border: "none",
  cursor: "pointer",
  transition: "opacity 0.2s ease",
  ":hover": {
    opacity: 0.9,
  },
});

Apply the theme to your layout:

---
import { lightTheme, darkTheme } from "../styles/theme.css";
---

<html class={lightTheme}>
  <body>
    <slot />
  </body>
</html>

Recipes for Variants

The Recipes API lets you define component variants in a type-safe way:

// src/styles/badge.css.ts
import { recipe } from "@vanilla-extract/recipes";
import { vars } from "./theme.css";

export const badge = recipe({
  base: {
    display: "inline-flex",
    alignItems: "center",
    borderRadius: vars.radius.full,
    fontSize: "0.75rem",
    fontWeight: 600,
    textTransform: "uppercase",
    letterSpacing: "0.05em",
  },
  variants: {
    color: {
      primary: { backgroundColor: "#dbeafe", color: "#1d4ed8" },
      success: { backgroundColor: "#dcfce7", color: "#166534" },
      warning: { backgroundColor: "#fef3c7", color: "#92400e" },
      error: { backgroundColor: "#fce4ec", color: "#b71c1c" },
    },
    size: {
      sm: { padding: "2px 6px", fontSize: "0.625rem" },
      md: { padding: "4px 8px", fontSize: "0.75rem" },
      lg: { padding: "6px 12px", fontSize: "0.875rem" },
    },
  },
  defaultVariants: {
    color: "primary",
    size: "md",
  },
});

Use it with full type safety:

---
import { badge } from "../styles/badge.css";
---

<span class={badge({ color: "success", size: "sm" })}>Published</span>
<span class={badge({ color: "warning" })}>Draft</span>
<span class={badge({ color: "error", size: "lg" })}>Deleted</span>

Sprinkles for Utility Classes

Sprinkles generates a set of utility classes from your theme tokens, similar to Tailwind but fully type-safe:

// src/styles/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"],
    flexDirection: ["row", "column"],
    gap: vars.space,
    padding: vars.space,
    fontSize: {
      sm: "0.875rem",
      base: "1rem",
      lg: "1.125rem",
      xl: "1.25rem",
    },
  },
});

const colorProperties = defineProperties({
  properties: {
    color: vars.color,
    backgroundColor: vars.color,
  },
});

export const sprinkles = createSprinkles(responsiveProperties, colorProperties);

Production Tips

  1. Keep styles colocated. Put .css.ts files next to the components that use them. This makes it easy to find and update styles when refactoring.

  2. Use theme contracts for consistency. Define your design tokens in a theme contract. This enforces that all theme variations (light, dark, branded) provide every required value.

  3. Prefer recipes over conditionals. Instead of switching between class names with JavaScript, use the Recipes API to define variants declaratively. The output is static CSS, not runtime logic.

  4. Watch file sizes. Vanilla Extract generates one class per style call. For large design systems, this can produce many small classes. Monitor your CSS bundle size with tools like source-map-explorer.

  5. Use globalStyle sparingly. Vanilla Extract provides globalStyle() for targeting elements outside your component tree (like reset styles). Keep it to your global layout file to avoid specificity issues.

Alternatives to Consider

  • Tailwind CSS if you prefer utility-first classes without needing to write TypeScript files for styles.
  • CSS Modules if you want scoped styles with plain CSS syntax and no additional build tooling.
  • Panda CSS if you want a similar type-safe approach with built-in utility classes and a more runtime-like API.

Wrapping Up

Vanilla Extract brings type safety to CSS without any runtime overhead. For Astro projects, this means your styles are validated at build time, your theme tokens are enforced by TypeScript, and the output is static CSS that performs like hand-written stylesheets. The learning curve is minimal if you already know CSS-in-JS patterns, and the Recipes and Sprinkles APIs give you the building blocks for a complete design system. If you value type safety and zero-runtime CSS, Vanilla Extract is the tool to reach for.