3

TailwindCSS v4 has changed significantly the light/dark theme design due to the removal of tailwind.config.js file. In TailwindCSS v3 this is how I changed the custom CSS properties depending on the theme:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 4%;
    --foreground: 0 0% 98%;
  }
  .dark {
    --background: 0 0% 98%;
    --foreground: 0 0% 4%;
  }
}

However, in TailwindCSS v4, the @theme, @variant and @custom-variant keywords have been introduced. I have read the docs and tried to use them for getting the same result, for example: (This does NOT work)

@import "tailwindcss";
@import "@fontsource-variable/montserrat";

@theme {
  --color-foreground: 0 0% 8%;
  --color-background: 0 0% 98%;
}

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

@variant dark {
  @theme {
    --color-foreground: 0 0% 98%;
    --color-background: 0 0% 3.9%;
  }
}

Which is the correct way of doing this in TailwindCSS v4?

Adding custom variants docs

1

4 Answers 4

2

First of all, @theme is only needed once globally. After that, @theme provides global CSS variables that you can override:

input.css (WARNING: this is still invalid CSS, but the explanation of the example continues)

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

@theme {
  --color-foreground: 0 0% 8%;
  --color-background: 0 0% 98%;
}

@variant dark {
  --color-foreground: 0 0% 98%;
  --color-background: 0 0% 3.9%;
}

A CSS selector replaces @variant dark; in our case, the compiled CSS looks like this:

Generated CSS

:root, :host {
  --color-foreground: 0 0% 8%;
  --color-background: 0 0% 98%;
}

&:where(.dark, .dark *) {
  --color-foreground: 0 0% 98%;
  --color-background: 0 0% 3.9%;
}

It's clear that the CSS selector is invalid because the & requires a parent declaration, for example like this:

input.css (Valid, usable example)

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

@theme {
  --color-foreground: 0 0% 8%;
  --color-background: 0 0% 98%;
}

:root, :host {
  @variant dark {
    --color-foreground: 0 0% 98%;
    --color-background: 0 0% 3.9%;
  }
}

Generated CSS

:root, :host {
  --color-foreground: 0 0% 8%;
  --color-background: 0 0% 98%;

  &:where(.dark, .dark *) {
    --color-foreground: 0 0% 98%;
    --color-background: 0 0% 3.9%;
  }
}

References:

Example for more themes

It's always necessary to declare the colors in @theme, and then you can override them with CSS using the appropriate selectors for a specific theme.

For a more consistent declaration, I always recommend using @layer theme, but it's not mandatory. However, in that case, unlayered CSS will always have higher specificity than any layer.

let currentTheme;
const body = document.body;

function setLightTheme() {
  body.setAttribute('data-theme', 'light'); // not declared in style thats default
  currentTheme = 'light';
}

function setDarkTheme() {
  body.setAttribute('data-theme', 'dark');
  currentTheme = 'dark';
}

function setCoffeeTheme() {
  body.setAttribute('data-theme', 'coffee');
  currentTheme = 'coffee';
}

function toggleTheme() {
  if (currentTheme === 'light') {
    setDarkTheme(); // Switch to dark theme
  } else if (currentTheme === 'dark') {
    setCoffeeTheme(); // Switch to coffee theme
  } else {
    setLightTheme(); // Switch to light theme
  }
}

// This way, we can set themes based on custom parameters. For example, you can take into account the browser's preferred light/dark mode, the favorite theme saved by a logged-in user, etc.
setLightTheme(); // Set light theme first time
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>

<style type="text/tailwindcss" id="theme-style">
@theme {
  --color-foreground: hsl(0 0% 8%);
  --color-background: hsl(0 0% 98%);
}
@layer theme {
  [data-theme='dark'] {
    --color-foreground: hsl(0 0% 98%);
    --color-background: hsl(0 0% 3.9%);
  }
  [data-theme='coffee'] {
    --color-foreground: hsl(30 50% 60%);
    --color-background: hsl(30 30% 20%);
  }
}
</style>

<div class="flex flex-col gap-4 m-4">
  <button
    class="px-4 py-2 rounded-lg border bg-background text-foreground cursor-pointer"
    onclick="toggleTheme()"
  >
    Toggle light/dark/coffee mode
  </button>
  <p class="bg-background text-foreground p-4 rounded-lg">
    This is a themed paragraph. The theme changes dynamically.
  </p>
</div>

And with @variant instead of CSS selectors:

let currentTheme;
const body = document.body;

function setLightTheme() {
  body.setAttribute('data-theme', 'light'); // not declared in style thats default
  currentTheme = 'light';
}

function setDarkTheme() {
  body.setAttribute('data-theme', 'dark');
  currentTheme = 'dark';
}

function setCoffeeTheme() {
  body.setAttribute('data-theme', 'coffee');
  currentTheme = 'coffee';
}

function toggleTheme() {
  if (currentTheme === 'light') {
    setDarkTheme(); // Switch to dark theme
  } else if (currentTheme === 'dark') {
    setCoffeeTheme(); // Switch to coffee theme
  } else {
    setLightTheme(); // Switch to light theme
  }
}

// This way, we can set themes based on custom parameters. For example, you can take into account the browser's preferred light/dark mode, the favorite theme saved by a logged-in user, etc.
setLightTheme(); // Set light theme first time
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>

<style type="text/tailwindcss" id="theme-style">
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
@custom-variant coffee (&:where([data-theme=coffee], [data-theme=coffee] *));

@theme {
  --color-foreground: hsl(0 0% 8%);
  --color-background: hsl(0 0% 98%);
}
@layer theme {
  * {
    @variant dark {
      --color-foreground: hsl(0 0% 98%);
      --color-background: hsl(0 0% 3.9%);
    }
    @variant coffee {
      --color-foreground: hsl(30 50% 60%);
      --color-background: hsl(30 30% 20%);
    }
  }
}
</style>

<div class="flex flex-col gap-4 m-4">
  <button
    class="px-4 py-2 rounded-lg border bg-background text-foreground cursor-pointer"
    onclick="toggleTheme()"
  >
    Toggle light/dark/coffee mode
  </button>
  <p class="bg-background text-foreground p-4 rounded-lg">
    This is a themed paragraph. The theme changes dynamically.
  </p>
</div>

Sign up to request clarification or add additional context in comments.

Comments

1

In Tailwind v4, you use the @theme directive to define your default custom properties (CSS variables) and the @variant directive to modify them based on themes like dark. For dark mode, Tailwind automatically applies the dark variant if the dark class is present on the root element. You no longer manually toggle CSS with .dark inside your CSS file; instead, use these directives to handle it.

Comments

1

I was suffering from this and I had to take some time but I figured it out and it is based on the documentation here.

https://tailwindcss.com/docs/colors#using-a-custom-palette

I will share my example of my css file with the new tailwind v4 and hopefully that might be of help to anyone so far. This is my first time ever commenting on StackOverflow so hope its good code lol.

@import "tailwindcss";

:root {
  --background: hsl(276 10% 95%);
  --foreground: hsl(276 5% 10%);
  --primary: hsl(276 100% 50%);
  --primary-foreground: hsl(0 0% 100%);
  --secondary: hsl(276 10% 70%);
  --secondary-foreground: hsl(0 0% 0%);
  --muted: hsl(238 10% 85%);
  --muted-foreground: hsl(276 5% 40%);
  --destructive: hsl(0 50% 50%);
  --destructive-foreground: hsl(276 5% 90%);
  --border: hsl(276 20% 55%);
  --input: hsl(276 20% 50%);
  --ring: hsl(276 100% 50%);
}

.dark {
  --background: hsl(276 10% 10%);
  --foreground: hsl(276 5% 90%);
  --primary: hsl(276 100% 50%);
  --primary-foreground: hsl(0 0% 100%);
  --secondary: hsl(276 10% 20%);
  --secondary-foreground: hsl(0 0% 100%);
  --muted: hsl(238 10% 25%);
  --muted-foreground: hsl(276 5% 60%);
  --destructive: hsl(0 50% 50%);
  --destructive-foreground: hsl(276 5% 90%);
  --border: hsl(276 20% 50%);
  --input: hsl(276 20% 50%);
  --ring: hsl(276 100% 50%);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-destructive: var(--destructive);
  --color-destructive-foreground: var(--destructive-foreground);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
}

Essentially you just declare values in root and .dark as in normal css and then tag them with --color- . This --color- is necessary for tailwind so you just use colors like any other color thats part of tailwind. I think thats the case. It works as expected. I do not know what that inline does in there? Would love to see a response to that if anyone knows.

Thank you!

Comments

1

I found an article which shows a way of doing what I need, albeit the code is a bit verbose and uses the data-theme attribute instead of a CSS class, but it works like a charm:

@import "tailwindcss";

@theme {
  --color-foreground: var(--theme-color-foreground);
  --color-background: var(--theme-color-background);
}

@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

@layer base {
  [data-theme="light"] {
    --theme-color-foreground: hsl(0 0% 8%);
    --theme-color-background: hsl(0 0% 98%);
  }

  [data-theme="dark"] {
    --theme-color-foreground: hsl(0 0% 98%);
    --theme-color-background: hsl(0 0% 3.9%);
  }
}

I am open for better solutions!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.