1

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?

1 Answer 1

1

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.

Statically written utility names

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>;
}

"Dynamically" written utility names (Statically but you can manipulate)

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.

Color variables

Our additional task, then, is to ensure the availability of the colors.

Include global variables by @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>

Safelist (not recommended)

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>

Spacing

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>

Static theme (use @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>

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

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.