I'm looking for a canonical answer for Tailwind CSS v3 with the finalized JIT engine, which can no longer be modified in v4. I'd like to reference colors using a CSS variable and use a syntax like this:
<div class={`border-[${borderWidth}] text-${colorName}`}>...</div>
const borderWidth = "4px";
const colorName = "sky-500";
I understand that I cannot do this directly, because the essence of JIT is that TailwindCSS looks at the source files and does not generate the necessary CSS at runtime, so it has no knowledge of the variable's runtime value when generating the CSS. How can I still reference utilities dynamically using a JS variable, in a way that applies the class in the class attribute according to TailwindCSS's intended approach?
Exactly, TailwindCSS strongly opposes the syntax you mentioned. In fact, it's recommended to completely forget the word "dynamic" when it comes to TailwindCSS, because TailwindCSS is static and can only rely on CSS features, since the generated code is created during the production build and cannot be influenced at runtime. The advantage of this approach is that generation doesn't need to be done unnecessarily on every page load, and the same result doesn't have to be produced repeatedly, so the behavior isn't being altered.
The previously mentioned TailwindCSS documentation suggests a few reasonable alternatives. One of them is an if-else switch, which is probably indeed complicated in your case, but it statically includes the utility name for both the true and false values, so it works regardless:
<div class="{{ error ? 'text-red-600' : 'text-green-600' }}"></div>
Another example is introducing so-called enums, where you assign the required styles to a certain variant name in an object and, again, statically type out the name of each utility:
function Button({ color, children }) {
const colorVariants = {
blue: "bg-blue-600 hover:bg-blue-500",
red: "bg-red-600 hover:bg-red-500",
};
return <button className={`${colorVariants[color]} ...`}>{children}</button>;
}
Instead of relying on a JS framework, you can use an arbitrary variant (a feature available since TailwindCSS v3.1). This allows you to specify arbitrarily when a given class should become active; for example, you can rely on data attributes or the presence of other classes:
function Button({ color, children }) {
return <button
data-theme={color}
className={`
data-[theme=blue]:bg-blue-600 data-[theme=blue]:hover:bg-blue-500
data-[theme=red]:bg-red-600 data-[theme=red]:hover:bg-red-500
`}
>{children}</button>;
}
What you actually need is for an external variable, when given the appropriate value, to generate the corresponding utility and have it work. This is impossible in the form mentioned in the question. As I said, TailwindCSS can only rely on CSS functionality, so a reasonable approach is to use CSS variables, which allows a TailwindCSS utility name to be statically typed without JS variable:
<div class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}>...</div>
Then the value of the CSS variable can be manipulated dynamically:
<div
class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}
style={{
"--my-border-width": borderWidth,
"--my-border-color": `var(--color-${borderColorName})`,
"--my-text-color": `var(--color-${colorName})`,
}}
>
...
</div>
const borderWidth = "4px";
const borderColorName = "amber-400";
const colorName = "sky-500";
Note: In the case of CSS variables, you can observe that with border-(--variable) you cannot determine whether the variable provides a length, a color, or something else. In this case, TailwindCSS accepts length: or color: declarations as a prefix to precisely specify the type of the variable.
Important: Colors are a prominent example here. Although variables exist in TailwindCSS, if you haven't used, for instance, the *-sky-500 color even once, the --color-sky-500 global variable will not be included in the generated CSS.
Our additional task, then, is to ensure the availability of the colors.
@theme static (recommended)The default @theme creates global variables for the namespace, but only if the color has been used at least once. This is acceptable in most cases; however, for the current example to work, @theme static is needed, which ensures the global variable even if it hasn't been declared at all. This requires redefining all the default colors, but it will not cause duplication in the generated CSS.
Note: Of course, you don't have to move every color to static - only those whose variables you will definitely need.
function App() {
const borderWidth = "4px";
const borderColorName = "amber-400";
const colorName = "sky-500";
return (
<div
class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}
style={{
"--my-border-width": borderWidth,
"--my-border-color": `var(--color-${borderColorName})`,
"--my-text-color": `var(--color-${colorName})`,
}}
>
Hello World
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root')).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@theme static {
--color-amber-400: oklch(0.828 0.189 84.429);
--color-amber-500: oklch(0.769 0.188 70.08);
--color-sky-400: oklch(0.746 0.16 232.661);
--color-sky-500: oklch(0.685 0.169 237.323);
}
</style>
<div id="root"></div>
You can do this using @source inline (...);, that is, with the safelist, but it's not recommended, because it generates the actual utilities as well, not just the variables, which can significantly increase the size of the generated CSS by including many unused utilities.
function App() {
const borderWidth = "4px";
const borderColorName = "amber-400";
const colorName = "sky-500";
return (
<div
class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}
style={{
"--my-border-width": borderWidth,
"--my-border-color": `var(--color-${borderColorName})`,
"--my-text-color": `var(--color-${colorName})`,
}}
>
Hello World
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root')).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@source inline('{hover:,}{text,bg,border}-{amber,sky}-{50,{100..900..100},950}');
</style>
<div id="root"></div>
After colors, the next common question is generating values for utilities like margin, padding, width, height, etc., dynamically. If you don't want to specify the border-width manually in pixels, but want to reference something like border-4, you can do that.
Unfortunately, providing just the number is not enough, so border-${borderNumber} is still invalid syntax. However, you can understand how it works. From TailwindCSS v4 onward, a global --spacing variable can be used as a multiplier to generate padding, margin, height, width, and even border-width, etc.
border-[calc(var(--spacing) * 4)]
Note: If you've used at least one utility that requires the --spacing variable, you don't need to make it static separately; the default value is 0.25rem. In minimal examples, it's common to move it to static.
@theme static {
--spacing: 0.25rem;
}
In this case, the JS variable needs to be passed with a calculation, like this:
function App() {
const borderNumber = 4;
const borderColorName = "emerald-500";
const colorName = "emerald-950";
return (
<div
class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}
style={{
"--my-border-width": `calc(var(--spacing) * ${borderNumber})`,
"--my-border-color": `var(--color-${borderColorName})`,
"--my-text-color": `var(--color-${colorName})`,
}}
>
Hello World
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root')).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@theme static {
--spacing: 0.25rem;
--color-emerald-400: oklch(0.765 0.177 163.223);
--color-emerald-500: oklch(0.696 0.17 162.48);
--color-emerald-950: oklch(0.262 0.051 172.552);
}
</style>
<div id="root"></div>
@theme static instead of @theme by default)Or if you want to include all variables by default in the generated CSS (colors, spacing), then during import there is a TailwindCSS v4-specific syntax that allows us to do so:
@import "tailwindcss" theme(static);
function App() {
const borderNumber = 4;
const borderColorName = "emerald-500";
const colorName = "emerald-950";
return (
<div
class={`border-(length:--my-border-width) border-(color:--my-border-color) text-(--my-text-color)`}
style={{
"--my-border-width": `calc(var(--spacing) * ${borderNumber})`,
"--my-border-color": `var(--color-${borderColorName})`,
"--my-text-color": `var(--color-${colorName})`,
}}
>
Hello World
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root')).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss" theme(static); /* instead of @import "tailwindcss"; */
</style>
<div id="root"></div>