I have an application getting migrated from Tailwindcss v3 to v4.1.
I make use of color themes, dynamically switched by the client using data attribute or class on the <body>
. Thus elements can simply be styled using var(--primary-500)
, or even bg-neutral-100 text-primary-500 border-primary-500
, while the actual color palette is determined dynamically.
Here's an outline of the theme system:
//color-schemes.js - which is imported into the tailwind.config.js
module.exports = {
blue: {
primary: {
50: "#f1f9ff",
100: "#d9efff",
200: "#b1dfff",
},
accent: {
50: "#fff6ed",
100: "#ffedd5",
200: "#fed7aa",
},
neutral: {
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
},
},
green: {
primary: {
50: "#f6faf3",
100: "#e3f1dc",
},
accent: {
50: "#ecfdf5",
100: "#d1fae5",
},
}
...etc
//blue.css
[data-theme="blue"] {
--primary-50: theme("colors.blue.primary.50");
--primary-100: theme("colors.blue.primary.100");
--primary-200: theme("colors.blue.primary.200");
--secondary-50: theme("colors.blue.secondary.50");
--secondary-100: theme("colors.blue.secondary.100");
--secondary-200: theme("colors.blue.secondary.200");
}
//green.css
[data-theme="green"] {
--primary-50: theme("colors.green.primary.50");
--primary-100: theme("colors.green.primary.100");
--primary-200: theme("colors.green.primary.200");
--secondary-50: theme("colors.green.secondary.50");
--secondary-100: theme("colors.green.secondary.100");
--secondary-200: theme("colors.green.secondary.200");
}
I'm now unable to get this to work in v4 as config.js is discouraged. What is the css-first way to achieve this?
First and foremost, you need to declare the color for TailwindCSS in the @theme
, regardless of where it will be used. Once that's done, in some cases you can override the color.
/* declare primary-50 ... primary-900 etc. */
@theme {
--color-primary-50: #111;
--color-primary-900: #999;
}
After that, you can override the colors inside the @layer theme
like this:
/* overwrite default primary values */
@layer theme {
[data-theme="blue"] {
--color-primary-50: #f1f9ff;
--color-primary-900: #3f88c4;
}
[data-theme="green"] {
--color-primary-50: #f6faf3;
--color-primary-900: #1d843b;
}
}
Example with global variables:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";
:root {
--default-primary-50: #d8ade5;
--default-primary-900: #ae1fde;
--blue-primary-50: #f1f9ff;
--blue-primary-900: #3f88c4;
--green-primary-50: #f6faf3;
--green-primary-900: #1d843b;
}
@theme {
--color-primary-50: var(--default-primary-50);
--color-primary-900: var(--default-primary-900);
}
@layer theme {
[data-theme="blue"] {
--color-primary-50: var(--blue-primary-50);
--color-primary-900: var(--blue-primary-900);
}
[data-theme="green"] {
--color-primary-50: var(--green-primary-50);
--color-primary-900: var(--green-primary-900);
}
}
button {
@apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>
<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>
<div class="mt-4 p-4 bg-primary-50 text-primary-900 font-bold">
Hello World
</div>
I worked with three themes: default (when there is no data-theme), and blue and green (when the corresponding data-theme is set).
You can optionally declare custom variants for the data-themes:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";
@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));
:root {
--default-primary-50: #d8ade5;
--default-primary-900: #ae1fde;
--blue-primary-50: #f1f9ff;
--blue-primary-900: #3f88c4;
--green-primary-50: #f6faf3;
--green-primary-900: #1d843b;
}
@theme {
--color-primary-50: var(--default-primary-50);
--color-primary-900: var(--default-primary-900);
}
@layer theme {
[data-theme="blue"] {
--color-primary-50: var(--blue-primary-50);
--color-primary-900: var(--blue-primary-900);
}
[data-theme="green"] {
--color-primary-50: var(--green-primary-50);
--color-primary-900: var(--green-primary-900);
}
}
button {
@apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>
<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>
<div class="
mt-4 p-4 font-bold
bg-primary-50 text-primary-900
green:bg-primary-900 green:text-primary-50
">
Hello World
</div>
And just as a fun fact, you can also use @custom-variants
together with @variant
.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";
@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));
@theme {
--color-primary-50: #d8ade5;
--color-primary-900: #ae1fde;
}
@layer theme {
* {
@variant blue {
--color-primary-50: #f1f9ff;
--color-primary-900: #3f88c4;
}
@variant green {
--color-primary-50: #f6faf3;
--color-primary-900: #1d843b;
}
}
}
button {
@apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>
<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>
<div class="
mt-4 p-4 font-bold
bg-primary-50 text-primary-900
blue:uppercase
green:lowercase
green:bg-primary-900 green:text-primary-50
">
Hello World
</div>
And if you need access to the blue, green, and default colors without having a theme selected:
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";
@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));
@theme {
--color-default-primary-50: #d8ade5;
--color-default-primary-900: #ae1fde;
--color-blue-primary-50: #f1f9ff;
--color-blue-primary-900: #3f88c4;
--color-green-primary-50: #f6faf3;
--color-green-primary-900: #1d843b;
--color-primary-50: var(--color-default-primary-50);
--color-primary-900: var(--color-default-primary-900);
}
@layer theme {
[data-theme="blue"] {
--color-primary-50: var(--color-blue-primary-50);
--color-primary-900: var(--color-blue-primary-900);
}
[data-theme="green"] {
--color-primary-50: var(--color-green-primary-50);
--color-primary-900: var(--color-green-primary-900);
}
}
button {
@apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>
<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>
<div class="
mt-4 p-4 font-bold
bg-primary-50 text-primary-900
green:bg-primary-900 green:text-primary-50
">
Hello World by current theme
</div>
<div class="
mt-4 p-4 font-bold
bg-default-primary-50 text-default-primary-900
">
Only Default
</div>
<div class="
mt-4 p-4 font-bold
bg-blue-primary-50 text-blue-primary-900
">
Only Blue
</div>
<div class="
mt-4 p-4 font-bold
bg-green-primary-50 text-green-primary-900
">
Only Green
</div>