Tailwind 4 Dark Mode Dynamic Theme

Learn how to implement a dynamic dark mode theme in Tailwind CSS 4 using the new theme in CSS config.

January 25, 2025

So the Tailwind 4 released. Most breaking change was introduction of new theme configuration method - you can only do it in CSS now. Here is an official example:

@import 'tailwindcss';

@theme {
  --font-display: 'Satoshi', 'sans-serif';
  --breakpoint-3xl: 1920px;
  --color-avocado-100: oklch(0.99 0 0);
  --color-avocado-200: oklch(0.98 0.04 113.22);
  --color-avocado-300: oklch(0.94 0.11 115.03);
  --color-avocado-400: oklch(0.92 0.19 114.08);
  --color-avocado-500: oklch(0.84 0.18 117.33);
  --color-avocado-600: oklch(0.53 0.12 118.34);
  --ease-fluid: cubic-bezier(0.3, 0, 0, 1);
  --ease-snappy: cubic-bezier(0.2, 0, 0, 1);
  /* ... */
}

The problem with this approach is that there is no easy way to define dark mode theme here as complained on their GitHub.

Solution

This is how I solved this issue in my React components system @creation-ui/react using @variant directive:

@import 'tailwindcss';

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

:root {
  --text-primary: oklch(0 0 0);
  --text-secondary: oklch(0.556 0 0);
  --background-primary: oklch(0.99 0 0);
  --background-secondary: oklch(0.985 0 0);
  --border: oklch(0.87 0 0);

  @variant dark {
    --text-primary: oklch(0.97 0 0);
    --text-secondary: oklch(0.87 0 0);
    --background-primary: oklch(0.269 0 0);
    --background-secondary: oklch(0.371 0 0);
    --border: oklch(0.35 0 0);
  }
}

@theme {
  --color-primary: oklch(60.48% 0.2165 257.21);
  --color-warning: oklch(77.97% 0.1665 72.45);
  --color-error: oklch(66.16% 0.2249 25.88);
  --color-success: oklch(75.14% 0.1514 166.5);

  --color-text-primary: var(--text-primary);
  --color-text-secondary: var(--text-secondary);
  --color-background-primary: var(--background-primary);
  --color-background-secondary: var(--background-secondary);
  --color-border: var(--border);
}

Basically in :root we define both our light and dark color variables where @variant dark decides of the variable values.

Because Tailwind generates color CSS classes (text-[color]-value, bg-[color]-value, border-[color]-value, etc.) from --color- variables, we can use them in our React components like so:

const Button = ({ children, ...props }) => {
  return (
    <button className={`bg-text-primary text-text-primary`} {...props}>
      {children}
    </button>
  )
}

And that's it! Now you can use dynamic light/dark theme in your app and it will automatically switch to dark mode when you set dark variant (here: a .dark class on e.g. body element).