Skip to main content
All articles
Guide August 10, 2026 8 min read

Building a Design System with Tailwind CSS v4 and CSS Variables

A practical guide to building a design system using Tailwind v4's CSS-first theming: semantic tokens, oklch colors, dark mode, and component patterns.

By Ninna UI Team


A design system is more than a component library — it's a set of tokens, patterns, and conventions that keep your UI consistent. Tailwind CSS v4's CSS-first model makes building one simpler than ever. Here's how.

1. Define semantic tokens

Start with semantic color tokens — names that describe purpose, not appearance. Instead of blue-500, use primary. Instead of red-500, use danger. This decouples meaning from color:

/* tokens.css */
:root {
  --color-primary: oklch(0.55 0.22 280);
  --color-primary-content: oklch(0.98 0.01 280);
  --color-success: oklch(0.60 0.18 145);
  --color-success-content: oklch(0.98 0.01 145);
  --color-warning: oklch(0.70 0.18 85);
  --color-warning-content: oklch(0.20 0.02 85);
  --color-danger: oklch(0.55 0.22 25);
  --color-danger-content: oklch(0.98 0.01 25);
  --color-base-100: oklch(0.98 0.005 280);
  --color-base-200: oklch(0.95 0.008 280);
  --color-base-content: oklch(0.25 0.02 280);
}

Each semantic color has a content pair — the text color that sits on top of it. This ensures contrast ratios are always correct.

2. Dark mode overrides

.dark {
  --color-primary: oklch(0.72 0.18 280);
  --color-primary-content: oklch(0.15 0.02 280);
  --color-base-100: oklch(0.18 0.02 280);
  --color-base-200: oklch(0.22 0.02 280);
  --color-base-content: oklch(0.92 0.01 280);
}

Dark mode is just different values for the same custom properties. The browser handles the switch — no JavaScript needed.

3. Map to Tailwind utilities

Tailwind v4's @theme directive maps CSS variables to utility classes:

@import "tailwindcss";

@theme {
  --color-primary: var(--color-primary);
  --color-primary-content: var(--color-primary-content);
  --color-success: var(--color-success);
  /* ... */
}

Now you can use bg-primary, text-primary-content, border-success, etc. in your components.

4. Component patterns

With tokens defined, build components that reference them. The key principle: components should never hardcode colors. Always use semantic tokens:

// Good: uses semantic tokens
<button className="bg-primary text-primary-content hover:bg-primary/90">
  Click me
</button>

// Bad: hardcodes colors
<button className="bg-blue-600 text-white hover:bg-blue-500">
  Click me
</button>

5. Theme presets

A theme preset is just a different set of values for the same custom properties. Create multiple preset files and swap the import to change the entire theme:

/* ocean.css */
:root {
  --color-primary: oklch(0.55 0.15 240);
  --color-primary-content: oklch(0.98 0.01 240);
}

/* sunset.css */
:root {
  --color-primary: oklch(0.62 0.20 30);
  --color-primary-content: oklch(0.98 0.01 30);
}

Ninna UI ships 5 theme presets using exactly this pattern: default, forest, sunset, ocean, and minimal. Each is under 50 lines of CSS.

6. Spacing and typography tokens

Beyond colors, define spacing and typography tokens. Tailwind v4's default spacing scale (1 = 0.25rem) is a good starting point. For typography, define font-family, font-size, and line-height as custom properties:

:root {
  --font-sans: "Inter", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", monospace;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
}

The result

You now have a design system: semantic color tokens with oklch, dark mode without JavaScript, theme presets via CSS imports, and consistent component patterns. This is exactly how Ninna UI is built — and you can use it as a reference for your own system.


Build it with Ninna UI

Accessible React components, CSS-only theming, Tailwind v4 native.

Get Started

Keep reading