Building a Design System with Tailwind CSS in Next.js
Tailwind without structure becomes utility soup. How to build a real design system in Next.js: tokens first, variant-driven components, and rules that keep it coherent.
Tailwind is a set of primitives, not a design system — the system is what you build on top. Left unstructured, a Tailwind codebase drifts into fourteen shades of grey, five slightly different buttons, and class strings nobody dares touch. The antidote is the same as it ever was: tokens, components and rules, expressed in Tailwind's vocabulary inside your Next.js project.
Tokens first
Decide your palette, type scale, spacing and radii before writing components, and encode them as CSS custom properties that Tailwind's theme maps onto. Semantic names beat literal ones: a background token can change for dark mode; a token named after a hex value cannot.
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 262 83% 58%;
--radius: 0.5rem;
}
.dark {
--background: 222 47% 7%;
--foreground: 210 40% 98%;
}With the theme mapped to variables, components write bg-background and text-foreground, and the entire system retunes — dark mode included — by changing token values in one file. This is also the layer where designers and developers share a vocabulary; the tokens are the contract.
On Tailwind v4 the mapping is even more direct: the theme lives in the CSS itself, so tokens and the utilities they generate are declared in one place. Whichever version you run, the principle holds — semantic names map onto variables, and raw values appear exactly once in the codebase.
Components with declared variants
Repeated class strings are the design system leaking. Each recurring pattern becomes a component with an explicit variant API, and class-variance-authority is the cleanest way to declare one — the component's visual options become typed props rather than tribal knowledge.
import { cva, type VariantProps } from 'class-variance-authority';
const button = cva(
'inline-flex items-center rounded-md font-medium transition-colors',
{
variants: {
intent: {
primary: 'bg-primary text-white hover:bg-primary/90',
ghost: 'hover:bg-muted',
},
size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4' },
},
defaultVariants: { intent: 'primary', size: 'md' },
}
);A pragmatic shortcut we endorse: shadcn/ui generates components built exactly this way, copied into your repo where you own and adapt them. It is a head start on the system, not a substitute for having one.
Resist prop explosion as the system grows. When a component sprouts its tenth boolean, it usually wants to be two components — or to expose composition through children and slots rather than another flag.
Server-first by construction
A pleasant side effect of Tailwind in the App Router: styling adds no client JavaScript. A well-built button, card or badge styled with utility classes can stay a Server Component, keeping 'use client' reserved for genuinely interactive pieces. Runtime CSS-in-JS libraries cannot say the same — many require client components to work at all, which is reason enough to prefer the Tailwind approach in Next.js.
Long-form content is the one sanctioned exception to component purity: for CMS-driven rich text, the typography plugin's prose classes style an entire article in one pass, with your tokens feeding its palette so it never drifts from the system.
Rules that keep it coherent
- Arbitrary values in square brackets are a code smell — if a value is needed twice, it is a token
- New visual patterns become components; pages compose components rather than raw utility stacks
- Merge classes with a helper built on tailwind-merge so overrides behave predictably
- Document components somewhere visible — Storybook or a simple internal route — so the path of least resistance is reuse
A design system is judged by the second year, not the second sprint: can a new developer ship a consistent page without asking anyone? Tokens, variants and a few enforced rules get you there. The framework will not police any of this — a short written standard and code review will.
STRCLI builds and rescues design systems for Next.js teams — if yours needs a firmer foundation, we should talk.