Skip to main content
Tailwind CSS Mastery·Lesson 3 of 5

Custom Themes

Tailwind ships with a carefully designed default theme, but real projects need custom branding. Tailwind makes it straightforward to extend or override every design token.

Tailwind CSS 4: Theme Configuration

In Tailwind CSS 4, theme customization is done directly in your CSS using the @theme directive instead of a JavaScript config file:

/* app/globals.css */
@import "tailwindcss";

@theme {
  --color-brand: #E21B1B;
  --color-brand-light: #FF4D4D;
  --color-brand-dark: #B01515;

  --color-surface: #FFFFFF;
  --color-surface-alt: #F8F9FA;

  --font-sans: "Ubuntu", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
  --font-serif: "Source Serif 4", ui-serif, Georgia, serif;

  --spacing-18: 4.5rem;
  --spacing-128: 32rem;

  --radius-card: 0.75rem;
}

These tokens immediately generate utilities like bg-brand, text-brand-light, font-sans, rounded-card, and w-128.

Custom Color Palette

Define a full color scale for your brand:

@theme {
  --color-brand-50: #FEF2F2;
  --color-brand-100: #FEE2E2;
  --color-brand-200: #FECACA;
  --color-brand-300: #FCA5A5;
  --color-brand-400: #F87171;
  --color-brand-500: #E21B1B;
  --color-brand-600: #DC2626;
  --color-brand-700: #B91C1C;
  --color-brand-800: #991B1B;
  --color-brand-900: #7F1D1D;
  --color-brand-950: #450A0A;
}

Now use these like any built-in color:

<button class="bg-brand-500 hover:bg-brand-600 text-white px-4 py-2 rounded">
  Subscribe
</button>

<div class="border border-brand-200 bg-brand-50 text-brand-800 p-4 rounded-lg">
  <p>Your trial expires in 3 days.</p>
</div>

Custom Fonts

Register font families in @theme and load them with @font-face or a service like Google Fonts:

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-display: "Cal Sans", "Inter", sans-serif;
}
<h1 class="font-display text-4xl font-bold">Welcome Back</h1>
<p class="font-sans text-gray-600">Here is your dashboard overview.</p>

Dark Mode

Tailwind supports dark mode via a CSS variant. In Tailwind CSS 4, configure it with @custom-variant:

@custom-variant dark (&:where(.dark, .dark *));

This makes the dark: variant apply when the dark class is on an ancestor element (typically <html>).

Toggling the Dark Class

function ThemeToggle() {
  const toggle = () => {
    const isDark = document.documentElement.classList.toggle("dark");
    localStorage.setItem("theme", isDark ? "dark" : "light");
  };

  return (
    <button onClick={toggle} class="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700">
      <span class="dark:hidden">Moon Icon</span>
      <span class="hidden dark:inline">Sun Icon</span>
    </button>
  );
}

Preventing Flash of Wrong Theme

Add a blocking script in <head> that reads the stored preference before the page renders:

<script>
  if (
    localStorage.theme === "dark" ||
    (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
  ) {
    document.documentElement.classList.add("dark");
  }
</script>

Styling for Dark Mode

Apply dark: variants alongside your default styles:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
  <div class="max-w-xl mx-auto p-8">
    <h1 class="text-2xl font-bold">Settings</h1>
    <div class="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
      <label class="flex items-center justify-between">
        <span class="text-gray-700 dark:text-gray-300">Email notifications</span>
        <input type="checkbox" class="rounded border-gray-300 dark:border-gray-600" />
      </label>
    </div>
  </div>
</div>

CSS Variables for Dynamic Theming

CSS variables let you change themes at runtime without recompiling CSS. Define variables and reference them in @theme:

:root {
  --ui-bg: #FFFFFF;
  --ui-text: #111827;
  --ui-accent: #2563EB;
}

.dark {
  --ui-bg: #0F172A;
  --ui-text: #F1F5F9;
  --ui-accent: #3B82F6;
}

@theme {
  --color-ui-bg: var(--ui-bg);
  --color-ui-text: var(--ui-text);
  --color-ui-accent: var(--ui-accent);
}
<div class="bg-ui-bg text-ui-text">
  <a href="#" class="text-ui-accent hover:underline">Learn more</a>
</div>

This technique also works for multi-brand theming — swap variables based on a class or data attribute:

[data-brand="acme"] {
  --ui-accent: #7C3AED;
}

[data-brand="globex"] {
  --ui-accent: #059669;
}
<body data-brand="acme">
  <button class="bg-ui-accent text-white px-4 py-2 rounded">
    Brand-specific button
  </button>
</body>