Detecting Classes in Source Files
Purpose and Scope
Tailwind CSS generates styles by finding class-like tokens in application source files, resolving the tokens against the design system, and emitting CSS only for the utilities that are actually used. For users, the important rule is that detection is text-based rather than language-aware: Tailwind does not evaluate JavaScript, JSX, templates, or string interpolation to discover possible values. For implementers, the repository separates the workflow into source discovery, candidate parsing and validation, optional migration-time rewriting, and source-map bookkeeping so that generated CSS can still point back to meaningful input positions.
The official docs frame this behavior as a zero-runtime build step: Tailwind scans HTML, JavaScript components, and other templates for class names, then writes the corresponding static CSS. This page connects that product model to the implementation surfaces visible in the repository. The strongest source evidence here is not a single scanner API, but the surrounding system pieces that make scanning reliable: Oxide source detection rules, design-system candidate parsing used by codemods, and line-table/source-map utilities used when compiler output needs traceability. Sources: crates/oxide/src/scanner/auto_source_detection.rs, packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts, packages/tailwindcss/src/source-maps/line-table.ts
Relevant Source Files
crates/oxide/src/scanner/auto_source_detection.rs- Defines default Gitignore-style rules used for automatic source detection, including ignored content directories, ignored extensions, binary extensions, and ignored files.packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts- Shows migration-time processing of raw class candidates throughdesignSystem.parseCandidateanddesignSystem.printCandidatefor simple renamed utilities.packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts- Shows deeper candidate migration that preserves variants and important flags while replacing the base utility candidate.packages/tailwindcss/src/source-maps/line-table.ts- Provides the offset-to-line-and-column lookup table used by source-map generation and diagnostics.packages/tailwindcss/src/source-maps/line-table.bench.ts- Benchmarks repeated line-table lookups against Tailwind source content, signaling that position lookup performance matters in compilation workflows.packages/@tailwindcss-node/src/source-maps.ts- Serializes decoded source maps, attaches original source content, and exposes raw, inline, and comment forms for Node integrations.
Detection Model
The user-facing detection model is intentionally simple: write complete utility class names in files that Tailwind scans. Because files are treated as plain text, a literal token like text-red-600 can be discovered, but a constructed expression that only becomes that string at runtime cannot. This explains the common guidance to map component props to complete class strings instead of concatenating fragments such as text-${color}-600. The compiler can only try tokens it sees in the source corpus; it then discards tokens that do not resolve to known utilities.
Automatic source detection is the repository-level mechanism that decides which files are likely worth scanning before class extraction happens. The Oxide scanner rules are built from fixture lists and compiled into ignore::gitignore::Gitignore matchers. The defaults ignore common content directories such as dependency and VCS folders, file extensions that Tailwind does not want to include, common binary extensions, and common lock or generated files. Binary extension rules are explicitly configured with only_on_files(true), which keeps those patterns from accidentally matching directory names. Sources: crates/oxide/src/scanner/auto_source_detection.rs
This division is important for both correctness and performance. Source detection narrows the search space so Tailwind spends time on templates and components rather than images, lockfiles, compiled assets, or dependency trees. Token extraction can then remain language-agnostic, which keeps it usable across HTML, JSX, Vue, Svelte, server-rendered templates, and other text formats. The tradeoff is that Tailwind cannot infer classes that are absent from the text. The practical authoring pattern is to make every possible class visible as a full token, even when choosing among them conditionally in application code.
Candidate Processing and Migration
After a class-like token is found, Tailwind treats it as a candidate: a raw string that may represent a static utility, a utility with variants, an important marker, or an arbitrary value. The upgrade codemods demonstrate this contract from outside the main scanner. migrateSimpleLegacyClasses receives a rawCandidate, registers removed v3 static utilities on the design system so they can be parsed, calls designSystem.parseCandidate, and prints a replacement candidate when the parsed candidate is a static legacy utility. This is a concise example of the pipeline boundary: scanning finds text, while the design system decides whether that text is meaningful Tailwind syntax. Sources: packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts
The more advanced legacy class migration shows why candidates are structured rather than treated as arbitrary strings forever. migrateLegacyClasses parses the raw candidate, reduces it to a base candidate such as blur, looks up the v4 replacement such as blur-sm, parses the replacement, clones it, and then reapplies the original variants and important flag. That preserves forms like hover:blur! while changing only the utility root. It also checks theme keys so that a replacement is safe with project customization. This mirrors normal compilation: the visible token is parsed into semantic parts before Tailwind decides what CSS, if any, should be generated. Sources: packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts
For application authors, this reinforces why variants and modifiers should be written as complete class names. A token such as dark:hover:bg-slate-800 is discoverable as text and can be parsed into variant components and a base utility. A token assembled from fragments may never reach the candidate parser in the intended form. For tooling authors, it means transformations should prefer parseCandidate and printCandidate where available instead of string replacement, because those APIs preserve Tailwind-specific structure such as variants, roots, and important markers.
Source Maps and Position Tracking
Class detection and CSS generation are more useful when integrations can relate generated output back to input sources. Tailwind's source-map support includes a compact line table that converts character offsets into one-based line and column positions. createLineTable computes the start offset of each line in one pass over the source, then answers find(offset) with a binary search. It also exposes findOffset(pos) for the reverse direction, clamping positions to the known source range. This is the low-level primitive that lets AST nodes store indexes while later reporting human-readable locations. Sources: packages/tailwindcss/src/source-maps/line-table.ts
The benchmark next to the line-table implementation repeatedly calls table.find(i) for every offset in a CSS file loaded from Tailwind's source tree. That benchmark is small, but it captures a real compiler concern: source maps and diagnostics can require many offset lookups, so the lookup structure must be fast enough not to dominate build time. The implementation comments call out the asymmetry directly: table creation is linear in source length, while each lookup is logarithmic in the number of lines. Sources: packages/tailwindcss/src/source-maps/line-table.bench.ts, packages/tailwindcss/src/source-maps/line-table.ts
Node integrations receive decoded maps and turn them into standard source-map payloads through toSourceMap. The serializer assigns stable source URLs, fills in missing sources as unknown placeholders, adds mappings to source-map-js, and stores original source content with setSourceContent. The returned object exposes raw, lazy inline, and comment(url) forms, so callers can either write an external map, embed a data URL, or append a sourceMappingURL comment. This is not class extraction itself, but it is part of the developer experience around scanned sources and generated CSS. Sources: packages/@tailwindcss-node/src/source-maps.ts
Practical Authoring Rules
The most reliable way to work with Tailwind detection is to keep every class complete in the source text. Prefer a lookup table of full strings over interpolation. For example, a component can choose between text-red-600 and text-green-600 as full values, but should not build text- plus a variable plus -600. The same applies to variants and arbitrary values: the final candidate should be visible to the scanner as one token-like string in the file that Tailwind reads.
When a class is not generated, debug the problem in the same order the system processes it. First, confirm that the file is included by source detection and is not under an ignored directory, ignored extension, binary extension, or ignored filename pattern. Second, confirm that the exact class exists as text, not only as a runtime result. Third, confirm that the candidate is valid for the active design system and theme. Migration tooling demonstrates the last step: it parses candidates against the design system and only rewrites candidates that have a known Tailwind meaning. Sources: crates/oxide/src/scanner/auto_source_detection.rs, packages/@tailwindcss-upgrade/src/codemods/template/migrate-simple-legacy-classes.ts
System-to-Code Mapping
| System concern | Repository implementation signal | What it means for developers |
|---|---|---|
| Source discovery | crates/oxide/src/scanner/auto_source_detection.rs | Tailwind avoids scanning dependency folders, binary assets, ignored extensions, and common generated files by default. |
| Candidate validation | designSystem.parseCandidate in upgrade codemods | A detected token must parse as a Tailwind candidate before it can be migrated or compiled. |
| Candidate printing | designSystem.printCandidate in upgrade codemods | Tools should preserve Tailwind syntax by printing structured candidates instead of hand-building strings. |
| Variant preservation | cloneCandidate, candidate.variants, and candidate.important in legacy migration | Rewrites can change the base utility while keeping prefixes like hover: and important markers intact. |
| Position mapping | createLineTable, find, and findOffset | Compiler internals can store offsets and later recover line and column positions. |
| Source-map output | toSourceMap, raw, inline, and comment(url) | Node integrations can expose generated CSS maps externally or inline. |
Next Steps
If you are using Tailwind in an application, review the utility-class authoring model next and make sure dynamic component APIs select from complete class strings. If you are integrating Tailwind into a build tool, follow the Node API and package integration pages to understand where scanned sources, dependency tracking, and source-map output surface in public APIs. If you are upgrading a project, the upgrade tooling pages explain how candidate parsing is reused to migrate old class names safely instead of relying on broad text replacement.