Building a Design System with Tailwind CSS v4
Tailwind v4 moved configuration into CSS. Learn the CSS-first theme system: design tokens, @theme, variants, and a component layer that scales across products.
Published on • August 10, 2026
AI Assistant

Design systems die two ways: as a sketch nobody implements, or as a pile of duplicated classes nobody can change. Tailwind CSS v4 attacks both by moving the whole configuration story into CSS. Where v3 hid your tokens in a tailwind.config.js that the rest of the codebase never looked at, v4 puts them in a @theme block — a single, versionable, lintable source of truth your components reference directly.
In this post, you will learn the CSS-first foundation of Tailwind v4, how to define design tokens with @theme, how to compose components without leaving the CSS file, and how to build a token set that survives a rebrand.
The v4 shift: CSS-first configuration
Tailwind v4 rearchitected around native CSS. A typical setup — Vite, no config file at all:
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ plugins: [tailwindcss()] });
/* src/index.css */
@import "tailwindcss";
No tailwind.config.js, no content globs. Tailwind scans your source for class names automatically and generates only what’s used. Configuration that used to be JavaScript is now CSS.
Design tokens with @theme
The heart of a design system is its tokens — color, spacing, typography, radii. In v4 you declare them once in @theme, and every utility derives from them:
@import "tailwindcss";
@theme {
/* Color */
--color-brand-50: #eef4ff;
--color-brand-500: #3b82f6;
--color-brand-900: #1e3a8a;
/* Spacing scale */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-4: 1rem;
/* Typography */
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--text-2xl: 1.5rem;
/* Radii & shadows */
--radius-lg: 0.5rem;
--shadow-card: 0 1px 3px rgb(0 0 0 / 0.1);
}
A token named --color-brand-500 immediately generates bg-brand-500, text-brand-500, border-brand-500, and every variant. Tokens named --spacing-4 become p-4, m-4, gap-4. The convention is the variable name is the API: --color-* → color utilities, --spacing-* → spacing, --font-* → fonts, --radius-* → radii, --shadow-* → shadows.
This means your brand is expressed in CSS, your utilities reference it, and a rebrand is a single file change:
@theme {
/* Swap the whole palette — every bg-brand-* in the app follows */
--color-brand-500: #7c3aed;
}
Building components with @layer
Tokens alone don’t stop people from writing text-sm font-medium text-gray-900 ten times. Compose them into component classes with @layer components:
@import "tailwindcss";
@theme {
--color-brand-500: #3b82f6;
/* ... */
}
@layer components {
.btn {
@apply inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold;
}
.btn-primary {
@apply bg-brand-500 text-white hover:bg-brand-600 focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-brand-500;
}
.btn-secondary {
@apply border border-gray-300 bg-white text-gray-900 hover:bg-gray-50;
}
.card {
@apply rounded-lg bg-white p-6 shadow-card;
}
.input {
@apply w-full rounded-lg border border-gray-300 px-3 py-2 text-sm
focus:border-brand-500 focus:outline-none;
}
}
Now your markup is semantic and stable while the implementation stays centralized:
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Cancel</button>
Change .btn-primary once and every button in the app updates. The @apply directive keeps the design tokens as the source of truth — you’re still using utilities, just in one place.
Variants: the state machine
v4 kept the variant system and made it more capable — hover, focus, responsive, dark mode, and arbitrary variants all work on top of tokens:
<button class="btn btn-primary">
<!--
hover:bg-brand-600 → state-based color
focus-visible:outline-2 → keyboard focus styles
dark:bg-brand-700 → dark mode token
sm:hover:shadow-card → responsive + state
-->
</button>
Dark mode in v4 is CSS-native: define your dark variant once and let it inherit from the token system:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-surface: #ffffff;
--color-ink: #0f172a;
}
.dark {
--color-surface: #0f172a;
--color-ink: #f8fafc;
}
Because tokens are CSS variables, swapping them in .dark re-skins the whole component layer with zero class changes.
Theme variables and the token contract
The most important v4 feature for a design system is that tokens are CSS variables at runtime. That means tokens are inspectable in DevTools, overrideable by theme, and consumable by anything — including inline styles and non-Tailwind code:
@theme inline {
--color-surface: oklch(0.98 0 0);
--color-ink: oklch(0.25 0.02 260);
}
Now components reference bg-surface, text-ink, and your team references the same tokens in Figma by name. The token contract — the list of --color-*, --font-*, --space-* variables your product is allowed to use — is the design system’s API. Keep it small, name it clearly, and everything downstream is consistent.
Putting It All Together
A complete design system starter — the @theme token set, @layer components with button/card/input/badge primitives, dark-mode variant, and an example page composing them. Drop it into a Vite project, run npm run dev, and you have a rebrandable foundation in one CSS file.
Conclusion & Next Steps
You now have a v4-native design system: tokens in @theme, components in @layer components, variants for state, and CSS variables as the runtime contract. Next steps: add motion tokens (a --ease-* scale and --animate-* keyframes), set up token linting so new utilities are flagged before merge, and export your @theme to Figma via the Design Tokens plugin so design and code share one source.
References / Sources
- Tailwind CSS v4 documentation — installation and theme variables. https://tailwindcss.com/docs
- Tailwind v4
@themereference. https://tailwindcss.com/docs/theme - Tailwind v4 styling with utility classes and
@layer. https://tailwindcss.com/docs/styling-with-utility-classes - Tailwind v4 dark mode and custom variants. https://tailwindcss.com/docs/dark-mode