how Tailwind CSS v4 finds every utility class
The Oxide Engine is the Rust based core of Tailwind CSS v4 and replaces the old JavaScript scanner with a much faster, memory conscious approach. Understanding how the Oxide Engine scans source code, extracts candidates, and recompiles incrementally helps you write content configurations more deliberately and confidently reason about unusual build behavior.
Table of Contents
- 1. What the Oxide Engine does differently
- 2. The scanning algorithm without a full AST
- 3. Candidate extraction: from raw text to utility candidates
- 4. Parsing candidates into AST nodes
- 5. Incremental rebuilding and the memory model
- 6. File type detection and language adapters
- 7. Scanner limits: what it cannot detect
- 8. Performance characteristics versus the v3 engine
- 9. Oxide Engine vs. classic JIT compiler compared
- 10. Summary
- 11. FAQ
1. What the Oxide Engine does differently
The Oxide Engine is the compiler foundation of Tailwind CSS v4 and was completely rewritten in Rust, while the previous version's engine ran in JavaScript. The switch is not merely a port: the Oxide Engine uses a fundamentally different approach to finding class names, one that does not require a full parser for every supported language. Instead of processing JSX, Vue templates, or PHP files each with their own language parser, the Oxide Engine first treats every source file as plain text and searches it for patterns that look like Tailwind classes.
This approach lets the Oxide Engine support practically any file extension without maintaining a dedicated parser for every framework. Whether Blade templates, Svelte components, Go templates, or raw HTML: the Oxide Engine looks for the same lexical patterns everywhere. This is why Tailwind CSS v4 works out of the box in practically any tech stack, while the predecessor engine needed special preprocessing steps for some template languages.
Choosing Rust as the implementation language for the Oxide Engine was not merely a matter of taste but a direct consequence of the requirements placed on the scanner. Rust allows memory safe, high performance text processing without a garbage collector, which is crucial for a process that potentially rescans thousands of files on every keystroke in watch mode. On top of that, the Oxide Engine can ship as a native binary with identical behavior across operating systems, reducing differences between a local development environment and a CI runner.
2. The scanning algorithm without a full AST
The scanning algorithm of the Oxide Engine deliberately does not work with a full abstract syntax tree of the source file. A full AST parser for JavaScript, TypeScript, JSX, Vue SFC files, and a dozen other formats would not only be expensive to maintain but also slow, because each parser would need to understand the complete syntax of its language even though only strings that look like class names matter. Instead, the Oxide Engine uses a specialized, very fast tokenizer that only recognizes character sequences that could match Tailwind's class name grammar.
This tokenizer works character by character over the raw file content and identifies word boundaries using quotes, whitespace, template literal delimiters, and similar separator symbols. It does not understand the meaning of the surrounding code, only that a given character sequence looks like hover:bg-sky-600 or md:grid-cols-[1fr_2fr]. This deliberate simplification is the core of what makes the Oxide Engine so much faster than a classic language parser: it does not need to understand the code, only search its text structure for known patterns.
/* Conceptually, the Oxide Engine's scanner treats source files as raw text
regardless of the surrounding language syntax. It looks for tokens
bounded by quotes, whitespace, or template delimiters that match
Tailwind's utility class grammar — no full JSX/Vue/PHP parser involved. */
/* All of these are found by the same text-based tokenizer, in totally
different host languages: */
/* JSX: className="hover:bg-sky-600 md:grid-cols-[1fr_2fr]" */
/* Vue: :class="['text-sky-700', isActive && 'font-bold']" */
/* Blade: class="{{ $active ? 'bg-sky-100' : 'bg-slate-100' }}" */
/* Go tpl: class="{{if .Active}}bg-sky-600{{end}}" */
3. Candidate extraction: from raw text to utility candidates
The second step of the Oxide Engine is internally called candidate extraction: character sequences recognized by the tokenizer become concrete utility candidates. Not every recognized string is automatically a valid Tailwind class, so this step checks every candidate against the known grammar of utility names, modifiers like hover: or md:, and square brackets for arbitrary values. A candidate like lg:hover:bg-[#0ea5e9]/50 gets decomposed into its parts: breakpoint modifier, pseudo class modifier, utility name, arbitrary value, and opacity modifier.
This decomposition does not happen through regular expressions in the classic sense, but through a hand written, stateful parser specifically optimized for Tailwind's class name grammar. The reason: pure regex approaches quickly become unreadable and slow given the complexity of modern Tailwind syntax with nested brackets, multiple modifiers, and arbitrary values. The specialized parser of the Oxide Engine can instead iterate through each candidate with linear runtime while simultaneously separating valid from invalid candidates, without the exponential backtracking that complex regex patterns can sometimes cause.
/* Conceptual breakdown the Oxide Engine performs on a single candidate: */
/* Input candidate: "lg:hover:bg-[#0ea5e9]/50" */
/* 1. Variant chain: lg: -> breakpoint modifier */
/* 2. Variant chain: hover: -> pseudo-class modifier */
/* 3. Utility root: bg- -> background-color property group */
/* 4. Arbitrary value: [#0ea5e9] -> literal color value */
/* 5. Opacity modifier: /50 -> alpha channel at 50% */
/* Result: a fully resolved utility ready for CSS generation,
without ever building a JS/TS/JSX abstract syntax tree. */
4. Parsing candidates into AST nodes
Only after a candidate has been recognized as grammatically valid does the Oxide Engine build a small, specific AST node for exactly that one utility class, not for the entire source file. This mini AST contains all the information the CSS generator needs: which CSS properties must be generated, which media queries or pseudo selectors result from the modifiers, and what the final selector must look like in the generated stylesheet.
This two phase approach, first text scan and candidate extraction, then targeted parsing only for recognized candidates, is the key to the speed of the Oxide Engine. A full language parser would need to convert the entire file into a tree structure even if only a fraction of the file actually contains Tailwind classes. The Oxide Engine, by contrast, only parses the text fragments that have already proven to be valid utility candidates, saving massive computation time on large files with a lot of non CSS code.
5. Incremental rebuilding and the memory model
For watch mode, the Oxide Engine keeps a persistent internal state across every file already scanned and the candidates it found. When a single file changes, only that file needs to be rescanned, while the candidate lists of all other files get reused from memory. The final CSS generation then combines the updated candidate set of the changed file with the unchanged candidate sets of every other file into a complete stylesheet.
This memory model explains why the Oxide Engine reacts within a few milliseconds in watch mode even on very large projects: the expensive part, the initial full scan of every file, happens only once at startup. Every further change is an incremental diff against the state held in memory. This contrasts with a naive full rescan approach where every file change would have to search the entire content tree again, which would mean noticeable delay across thousands of files.
# Conceptual timeline of the Oxide Engine's incremental model:
# 1. Cold start: scan every file matched by @source globs
# -> builds full candidate set, generates full CSS output
tailwind_engine.scan_all(sources) # expensive, happens once
# 2. Watch mode: a single file changes
# -> re-scan only that file, reuse cached candidates for the rest
tailwind_engine.rescan_file("src/Button.tsx") # cheap, milliseconds
# 3. Merge updated candidates with the cached candidate set
# -> regenerate only the CSS delta, not the entire stylesheet
tailwind_engine.merge_and_emit()
6. File type detection and language adapters
Although the Oxide Engine scans largely language agnostically, there are a few special case language adapters where pure text scanning is not enough. One example is detecting dynamically composed class names inside JavaScript template literals, where the Oxide Engine attempts to extract the static parts of a template string even if part of the string is a variable. Another special case involves CSS in JS libraries, where Tailwind classes sit inside function calls like clsx() or cn().
For file types that the Oxide Engine does not detect automatically, such as proprietary template languages or unusual file extensions, detection can be forced through explicit @source directives in the configuration. It is important to understand here that the Oxide Engine never executes code, it never evaluates a condition or a function call, it only searches textually for possible class names, regardless of whether the surrounding code is ever reached at runtime.
A concrete example of a language adapter is the handling of Vue single file components: a .vue file contains three separate blocks, template, script, and style, in a single file. The Oxide Engine scans all three blocks together as text, without knowing Vue compiler semantics, so it finds class names both in the template markup and in dynamically bound class objects inside the script block, as long as they exist as complete strings.
/* Forcing detection of an unusual file type the Oxide Engine does not
scan by default, e.g. a proprietary .liquid template extension */
@import "tailwindcss";
@source "./src/**/*.tsx";
/* Explicitly include a template language extension not covered by
the engine's built-in file type heuristics */
@source "./themes/**/*.liquid";
7. Scanner limits: what it cannot detect
The text based nature of the Oxide Engine has an important consequence: class names fully composed dynamically at runtime are not detected, because they simply do not exist as a complete string in the source code at build time. An expression like ` + "`bg-${color}-500`" + ` does not contain a complete class in the source text, only a fragment from which the Oxide Engine cannot derive a valid CSS rule. This is not an implementation limitation but a fundamental property of a static, build time based scanner.
For such cases, Tailwind CSS offers the safelist configuration as an escape hatch, explicitly including classes in the output regardless of the scan result. Anyone who understands that the Oxide Engine only analyzes static text will also immediately understand why fully dynamic string concatenation should generally be avoided in Tailwind projects, regardless of the framework or configuration used.
8. Performance characteristics versus the v3 engine
Moving from the JavaScript engine in Tailwind v3 to the Rust based Oxide Engine brings advantages mainly in two dimensions: raw scanning speed through compiled, native code instead of interpreted JavaScript, and significantly lower memory consumption through more efficient data structures for candidate management. In very large projects with thousands of source files, this difference shows not only in absolute numbers but also in consistency: the Oxide Engine shows less variance between individual build runs because it is not affected by JavaScript garbage collection and its variable pause times.
Another effect concerns cold start: because the Oxide Engine ships as a native binary, the time a JavaScript engine would have needed to parse and just in time compile its own scanner code disappears entirely. These characteristics add up especially in CI environments, where every build process starts cold anyway and cannot benefit from warm caches inside a running Node instance.
# Simple benchmark comparing cold-start scan time across engine versions
# Run each build 5 times and report the median, not a single sample
for i in 1 2 3 4 5; do
/usr/bin/time -f "%e s" npx tailwindcss -i input.css -o /dev/null 2>> times.log
done
sort -n times.log | awk 'NR==3 { print "Median scan time:", $0 }'
9. Oxide Engine vs. classic JIT compiler compared
The difference between the Oxide Engine and the classic JIT compiler approach from Tailwind v3 is best shown through concrete architecture decisions that have direct effects on speed, memory consumption, and language support.
| Aspect | v3 JIT engine (JavaScript) | Oxide Engine (Rust) |
|---|---|---|
| Scan method | Regex based text search | Hand written tokenizer |
| Language support | Framework specific adjustments needed | Largely language agnostic |
| Runtime environment | Node.js, JIT compiled on every start | Native binary, no cold start overhead |
| Memory management | Garbage collection with variable pauses | Deterministic, no GC overhead |
| Incrementality | Present, but slower on large trees | Persistent state, fast diffs |
This comparison shows that the Oxide Engine is not merely a reimplementation of the same idea, but a deliberate redesign of the entire scanning concept. The decision to forgo a full language parser and instead work with a specialized tokenizer is the central architecture choice that explains most of the observed speed advantages.
For plugin authors and maintainers of build integrations, this architecture shift also means that extending the Oxide Engine looks different than it did with the old JIT engine. Instead of hooking JavaScript callbacks into the scan process, extensions communicate with the Rust core through clearly defined interfaces, which raises the entry barrier somewhat but also prevents plugin code from accidentally undermining the performance characteristics of the Oxide Engine.
Mironsoft
Tailwind CSS architecture, migrations, and performance tuning
Content configuration that fits the Oxide Engine?
We analyze why your Tailwind classes are not being detected, set up content globs that match the Oxide Engine's scanning model, and fix safelist pitfalls before they end up missing from your production CSS.
Scanner diagnosis
Find missing utility classes in the build and explain the root cause in scanning behavior
Configuration review
Adapt content globs and safelist to the Oxide Engine's scanning model
v4 migration
Move existing v3 projects to the Oxide Engine and CSS first configuration
10. Summary
The Oxide Engine replaces the regex based JavaScript scanner in Tailwind CSS v4 with a specialized tokenizer written in Rust. Instead of fully parsing every source file into an AST, the Oxide Engine works in two phases: a fast text scan for candidate extraction, followed by targeted parsing only of the recognized candidates. This approach makes the Oxide Engine largely language agnostic and allows support for practically any template language without a dedicated parser.
For watch mode, the Oxide Engine keeps a persistent state that enables incremental rebuilds within milliseconds. Its limits lie where class names are dynamically composed at runtime, because a text based scanner cannot find a complete string in the source code. Anyone who understands this architecture can write content configurations more precisely and understand why certain classes are missing from the generated CSS instead of treating the behavior as a black box.
Oxide Engine Internals — Key Takeaways
Text instead of AST
The Oxide Engine scans source files as plain text without needing a full language parser.
Two phases
First candidate extraction via tokenizer, then targeted parsing only of recognized candidates.
Persistent state
Watch mode uses stored candidate lists and only rescans changed files.
Limits
Fully dynamic string concatenation at runtime is fundamentally never detected.