Skip to content

Tailwind CSS v4 Custom Themes — Creating Complete Design Systems

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Tailwind CSS v4 Custom Themes. We cover key concepts, practical examples, and best practices to help you master this topic.

Tailwind CSS v4 custom themes are built entirely in CSS using @theme for design tokens, @variant for theme-specific selectors, and CSS custom properties for dynamic theme switching.

What You'll Learn

You will learn how to create a complete design system with @theme, define semantics token names, create multiple themes (light/dark/brand variants), and organize theme files.

Why It Matters

A well-defined theme system ensures design consistency across products. DodaTech's v4 theme system powers Doda Browser, DodaZIP, and Durga Antivirus Pro with shared tokens.

Real-World Use

Durga Antivirus Pro uses a single @theme file with 60+ tokens covering colors, typography, spacing, and animation, shared across all products via npm package.

flowchart LR
    A[New Utilities] --> B[Custom Themes]
    B --> C[Design Tokens]
    B --> D[Semantic Names]
    B --> E[Multi-Theme]
    B --> F[Organization]
    style B fill:#38bdf8,stroke:#0284c7,color:#fff
    style C fill:#22c55e,stroke:#16a34a,color:#fff

Semantic Token Names

/* Use semantic names instead of color names */
@theme {
  /* Background colors */
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f9fafb;
  --color-bg-tertiary: #f3f4f6;

  /* Text colors */
  --color-text-primary: #111827;
  --color-text-secondary: #6b7280;
  --color-text-tertiary: #9ca3af;

  /* Interactive colors */
  --color-action-primary: #3b82f6;
  --color-action-primary-hover: #2563eb;
  --color-action-secondary: #6b7280;

  /* Semantic feedback */
  --color-success: #22c55e;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  --color-info: #3b82f6;
}
<div class="bg-bg-primary text-text-primary p-6 rounded-lg">
  <h2 class="text-text-primary font-bold">Semantic Theme</h2>
  <p class="text-text-secondary mt-2">Colors use semantic names, not visual names like blue or gray.</p>
  <button class="bg-action-primary hover:bg-action-primary-hover text-white px-4 py-2 rounded-lg mt-4">
    Primary Action
  </button>
</div>

Expected output: Components use semantic token names (bg-bg-primary, text-text-primary) instead of color-based names. Changing the token values changes the entire theme.

Multiple Themes

/* tokens.css */
@theme {
  --color-bg-primary: #ffffff;
  --color-text-primary: #111827;
  --color-action-primary: #3b82f6;
}

/* dark-theme.css */
@variant dark (&:where(.dark, .dark *));

.dark {
  --color-bg-primary: #111827;
  --color-text-primary: #f9fafb;
  --color-action-primary: #60a5fa;
}

/* brand-theme.css */
@variant brand (.brand &);

.brand {
  --color-bg-primary: #f5f3ff;
  --color-text-primary: #4c1d95;
  --color-action-primary: #7c3aed;
}
<div class="bg-bg-primary text-text-primary p-6 rounded-lg max-w-md mx-auto">
  <h2 class="font-bold">Theme Switcher</h2>
  <p class="mt-2 text-text-secondary">Switch themes by changing CSS variable values.</p>
  <button class="bg-action-primary text-white px-4 py-2 rounded-lg mt-4 font-medium">
    Action Button
  </button>
</div>

<!-- Add brand class to use brand theme -->
<div class="brand">
  <!-- Same component, different theme via CSS variables -->
</div>

Expected output: The same HTML with bg-bg-primary uses different colors depending on whether dark, brand, or default theme is active. CSS variable scoping handles the switch.

Theme Organization

/* tokens.css -- base design tokens */
@theme {
  /* Colors */
  --color-bg-primary: #ffffff;
  --color-text-primary: #111827;
  --color-action-primary: #3b82f6;

  /* Typography */
  --font-family-sans: "Inter", sans-serif;
  --font-family-display: "Playfair Display", serif;

  /* Spacing */
  --spacing-page: 2rem;
  --spacing-section: 5rem;
  --spacing-card: 1.5rem;

  /* Border radius */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;

  /* Shadows */
  --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1);
  --shadow-dropdown: 0 10px 15px -3px rgb(0 0 0 / 0.1);

  /* Animations */
  --animate-slide-in: slide-in 0.3s ease-out;
}

Expected output: A well-organized tokens.css file with sections for colors, typography, spacing, radius, shadows, and animations -- all in one place.

Typography Scale Theme

@theme {
  /* Custom type scale */
  --font-size-body: 1rem;
  --font-size-body-sm: 0.875rem;
  --font-size-h1: 2.5rem;
  --font-size-h2: 2rem;
  --font-size-h3: 1.5rem;
  --font-size-h4: 1.25rem;

  /* Line heights */
  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.75;
}

/* Usage in component styles */
h1 {
  font-size: var(--font-size-h1);
  line-height: var(--leading-tight);
  font-weight: 700;
}

.body-text {
  font-size: var(--font-size-body);
  line-height: var(--leading-relaxed);
}

Expected output: Typography tokens control the entire type scale. Changing --font-size-h1 updates all h1 elements.

Component-Specific Tokens

@theme {
  /* Button-specific tokens */
  --color-btn-primary-bg: #3b82f6;
  --color-btn-primary-text: #ffffff;
  --color-btn-primary-hover: #2563eb;
  --spacing-btn-padding-x: 1rem;
  --spacing-btn-padding-y: 0.5rem;
  --radius-btn: 0.5rem;

  /* Card tokens */
  --color-card-bg: #ffffff;
  --color-card-border: #e5e7eb;
  --spacing-card-padding: 1.5rem;
  --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1);

  /* Modal tokens */
  --color-modal-overlay: rgb(0 0 0 / 0.5);
  --spacing-modal-padding: 2rem;
  --animate-modal-enter: slide-up 0.3s ease-out;
}
<!-- Components use specific tokens instead of generic ones -->
<button class="bg-btn-primary-bg text-btn-primary-text px-btn-padding-x py-btn-padding-y rounded-btn hover:bg-btn-primary-hover transition-colors">
  Themed Button
</button>

<div class="bg-card-bg border border-card-border p-card-padding shadow-card rounded-lg">
  Themed Card
</div>

Expected output: Component-specific tokens (btn-, card-, modal-) keep component styles isolated. Changing btn tokens does not affect other components.

Theme Inheritance

/* base-theme.css */
@theme {
  --color-brand-500: #3b82f6;
  --font-family-sans: "Inter", sans-serif;
}

/* dark-theme.css -- inherits from base, overrides what it needs */
.dark {
  --color-brand-500: #60a5fa;
  /* font-family remains from base theme */
}

/* high-contrast-theme.css */
.high-contrast {
  --color-brand-500: #1d4ed8;
  --font-family-sans: "Atkinson Hyperlegible", sans-serif;
}

Expected output: Themes inherit base tokens and override specific values. Unchanged tokens fall through to the base theme value.

Common Mistakes

1. Using Visual Names Instead of Semantic

--color-blue-500 vs --color-action-primary. Semantic names make theme changes meaningful. Visual names require changing all HTML when the palette changes.

2. Not Scoping Theme Variables

Theme variables on :root apply globally. Use .dark, .brand, or [data-theme] scoping to avoid conflicts.

3. Over-Defining Tokens

Not every value needs a token. Hardcode one-off values. Tokens are for values used 3+ times across components.

4. Inconsistent Token Naming

Mix --btn-color and --color-btn creates confusion. Pick a convention like --component-property-value and stick to it.

5. Not Documenting Token Purpose

Each token should have a clear purpose. Comment tokens that are not self-explanatory: /* Primary action button background */.

Practice Questions

  1. What is a semantic token name? A name based on purpose, not appearance. Example: --color-action-primary instead of --color-blue-500.

  2. How do you create multiple themes in v4? Define base tokens in @theme, then override variables in scoped selectors like .dark, .brand.

  3. What is the benefit of component-specific tokens? Isolation. Changing btn-specific tokens does not affect card or modal components.

  4. How do tokens inherit across themes? Child themes override specific variables. Unchanged variables inherit from the parent theme.

  5. What should be tokenized? Values used in 3+ places: brand colors, spacing scale, font families, border radii, shadows, animation speeds.

Challenge

Create a complete design system with: semantic color tokens (10+), typography scale (5 sizes), component-specific tokens (button, card, modal), light and dark themes, and a high-contrast Accessibility theme.

FAQ

Can I share themes across projects?

Yes. Extract theme CSS files into an npm package and @import in each project.

How do I version themes?

Use semantic versioning for your theme package. Major version for breaking token changes.

Can themes be loaded dynamically?

Yes. Load theme CSS files as needed. Override variables on a container or :root.

How do I test theme accessibility?

Use contrast checkers for each theme's color pairs. Ensure all themes meet WCAG AA standards.

Can I use themes with CSS modules?

Yes. CSS modules work with theme variables. Import tokens.css and use var() references in module files.

Mini Project

Build a design system with: tokens.css (40+ design tokens), light and dark themes, component-specific button tokens (5 variants), typography scale, spacing system, and a theme switcher demo page showing all components in each theme.

What's Next

Now master Color Palette management in v4. Then explore Generators and Functions for dynamic color generation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro