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>