How Tailwind Finds Classes—and Why It Sometimes Cannot

Tailwind CSS’s source scanner decides which styling requests reach the compiler. This chapter examines that boundary: why Tailwind reads source files as text, why complete class strings matter, how arbitrary values remain build-time requests rather than runtime interpretation, and how explicit source controls change what the scanner is allowed to see. The goal is practical diagnosis: when a class is missing, determine first whether it was discovered at all.

The theme is only half of the compiler’s input. A project can define a perfectly valid token and still generate no rule if no detectable candidate requests it. Conversely, a complete candidate can be found and still fail later during utility or variant resolution. Separating those causes prevents a common debugging mistake—changing theme values to repair a source-discovery problem, or rewriting templates to repair a compiler problem.

The scanner reads tokens, not intentions

Tailwind treats source files as plain text. It does not try to understand the full semantics of JavaScript, JSX, Vue, Ruby, or another application language before extracting candidates. Instead, the Oxide scanner looks for spans that could be Tailwind class names, then passes those strings to the TypeScript compiler for validation. The design is intentionally narrower than program analysis: the scanner trades knowledge of runtime intent for a predictable build-time boundary.

That boundary makes complete class names essential. A literal such as text-red-600 exists in the source text as a complete candidate, so the scanner can return it. A template expression such as text-${color}-600 contains fragments, but it does not contain the concrete strings text-red-600 or text-green-600 that the compiler would need to resolve. The application may know the value of color at runtime; the scanner does not execute the application to discover it.

The reliable alternative is to map runtime choices to complete candidates. A component can associate blue with bg-blue-600 hover:bg-blue-500 and red with bg-red-500 hover:bg-red-400, then select one of those complete strings. This is more than a workaround for Tailwind. It makes the component’s styling contract explicit: each accepted value has a known set of utilities, and the build can see every class it may need to generate.

The mapping also gives a project a place to make deliberate design choices. The blue state can use a different shade from the red state, include a text color for contrast, or omit a hover treatment where it would be misleading. A direct interpolation would expose the raw value but hide those relationships. Static mappings keep the application’s runtime API separate from the compiler’s candidate language while still allowing the two to meet at a clear boundary.

A similar issue appears when classes are stored in data rather than markup. If a CMS payload contains a complete class string and that payload is part of a scanned source file, the scanner can potentially see it; if the value arrives only from an external service at runtime, it cannot. The question is not whether the final page will contain a class attribute.