Lesson 59 of 81
Tailwind never executes your code — it scans it as text for literal class name strings. A class built from a dynamic template string is invisible to that scan, and the bug it causes looks nothing like its cause.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Tailwind doesn't generate CSS for every class it could produce — that would be an enormous stylesheet. Instead, at build time it scans the files matched by its content globs as plain text, using a regex-based extractor to pull out anything that looks like a class name, and only generates CSS for the exact strings it finds. It never imports, parses as JavaScript, or executes any of those files — it just greps them.
That's the entire source of the classic bug: className={`text-${color}-500`} never appears anywhere in your source as the literal string text-red-500 or text-blue-500 — it appears as the template string itself. Tailwind's scanner doesn't know color will be "red" at runtime; it can't, because it isn't running your code. It just doesn't see that class as a string that exists, so it never generates CSS for it. No error, no warning — the class is simply absent from the final stylesheet, and the element renders unstyled.
This looks completely reasonable and silently fails:
// app/status-badge.tsx
function StatusBadge({ status }: { status: "ok" | "warn" | "error" }) {
const color = status === "ok" ? "green" : status === "warn" ? "yellow" : "red";
// Tailwind never sees "text-green-500", "text-yellow-500", or "text-red-500"
// as literal strings — only this template expression.
return <span className={`text-${color}-500`}>{status}</span>;
}
The fix is to write every class Tailwind needs to generate as a complete, literal string somewhere in the scanned source — a lookup object works well and doubles as documentation of exactly which variants exist:
// app/status-badge.tsx
const COLOR_BY_STATUS = {
ok: "text-green-500",
warn: "text-yellow-500",
error: "text-red-500",
} as const;
function StatusBadge({ status }: { status: keyof typeof COLOR_BY_STATUS }) {
return <span className={COLOR_BY_STATUS[status]}>{status}</span>;
}
Now text-green-500, text-yellow-500, and text-red-500 each appear as complete literal strings in a file the App Router build already includes in Tailwind's scan — the scanner finds all three regardless of which one status resolves to at runtime.
"text-" + color + "-500" is exactly as unscannable as the template-string version — the scanner is looking for complete literal strings, and neither approach produces one anywhere in the source.className={isActive ? "text-green-500" : "text-gray-400"} works, because both full strings are literally present in the file for the scanner to find — the failure mode is specifically about a class built by assembling pieces at runtime, not about conditional logic itself.Nothing about this is App-Router-specific — it's a direct consequence of how Tailwind's own content-scanning step works, which this track's build-system lesson touches on for Next.js's own compiler. The pattern to internalize generalizes past Tailwind: any tool that extracts information from your source by scanning it as text, rather than executing it, can only see what's written as a complete, literal value.