Conditional Classes in JSX
Toggling utility classes based on component state, and why string concatenation breaks Tailwind's build scan.
2 menit membaca
Real components change their appearance based on state — a selected tab, an invalid input, a loading button. Since Tailwind classes are just strings, applying them conditionally is ordinary JavaScript, with one important rule about how you build those strings.
The basic ternary pattern
function Tab({ label, isActive }) {
return (
<button
className={
isActive
? "border-b-2 border-blue-600 px-4 py-2 font-medium text-blue-600"
: "border-b-2 border-transparent px-4 py-2 font-medium text-slate-500 hover:text-slate-700"
}
>
{label}
</button>
);
}Each branch of the ternary is a complete, literal class string. That last part matters more than it looks: Tailwind's build step finds classes by scanning your source files for text that looks like a class name — it doesn't execute your JavaScript. Both full strings appear literally in the file, so both are found regardless of which branch actually runs at any given moment.
The mistake: building a class name from a variable
// Don't do this — Tailwind's scanner never sees "text-blue-600" or "text-slate-500"
// as a literal string, only "text-" and a variable next to it.
const color = isActive ? "blue-600" : "slate-500";
return <button className={`text-${color} font-medium`}>{label}</button>;This looks reasonable and even works correctly in a raw HTML preview with an unrelated stylesheet, but under Tailwind's actual build it silently fails: text-blue-600 and text-slate-500 never get generated, because neither ever appears as a complete string anywhere in your source. The fix is always to spell out full class names, even at the cost of a little repetition, as the first example does.
A small helper for longer class lists
Once a component has several conditional classes, a plain ternary chain gets hard to read. A tiny utility function — commonly named cn or clsx, after the popular clsx package — makes combining always-on and conditional classes clearer:
import clsx from "clsx";
function Button({ variant, disabled, children }) {
return (
<button
className={clsx(
"rounded-md px-4 py-2 text-sm font-medium",
variant === "primary" && "bg-blue-600 text-white hover:bg-blue-700",
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
disabled && "cursor-not-allowed opacity-50"
)}
disabled={disabled}
>
{children}
</button>
);
}clsx just joins truthy arguments into a single class string and drops falsy ones — it doesn't change the rule above at all, since every class inside it is still written as a complete literal string, just organized as separate arguments instead of one long ternary expression.
Keep every class name whole
Whichever pattern you reach for — a ternary, clsx, or a plain object map of variant names to class strings — the one constraint that never goes away is that every class name must exist somewhere in your source as unbroken, literal text. If you can't point to the exact string hover:bg-blue-700 sitting in a file, Tailwind's build has no way to know it should generate CSS for it.