Tailwind CSS 80 🎨 Framework Internals: How the Utility Scanner, Class Parser, and CSS Writer Work Together
The Tailwind engine is a pipeline of three stages. The scanner finds the class candidates in the source files. The parser resolves each candidate into a utility definition. The writer produces the CSS rules in the correct order. The three stages run at the build time, and the output is the stylesheet the browser loads. The earlier chapters covered the API and the patterns. This chapter goes into the internals: the data structures, the algorithms, and the order in which the work happens. The understanding is what makes the framework’s behavior predictable, and the debugging is the faster.
Key point: The scanner extracts the candidate strings without the understanding of the Tailwind syntax. The parser resolves the candidate against the utility registry. The writer emits the rules in the layer order. The three stages are the separate, and each has the own cost and the own failure mode.
Why the internals matter
The predictability problem. The class that does not generate the CSS is the common frustration. The internals explain why: the scanner did not find it, or the parser did not recognize it, or the writer did not emit it. The three have the different causes and the different fixes.
The performance problem. The build that is the slow is the three stages. The scanner that walks the large directory tree, the parser that resolves the thousands of the candidates, and the writer that emits the long stylesheet. The profile is the stage, and the optimization is the specific.
The debugging problem. The generated CSS is the output, and the internals are the input. The --watch log, the @source directive, and the candidate inspection are the tools. The knowing where the candidate is lost is the fix.
The correctness problem. The duplicate rule, the wrong specificity, and the missing declaration have the internal causes. The layer order, the variant composition, and the property conflict are the mechanisms, and the understanding is the verification.
The extension problem. The custom plugin registers the utilities, the variants, and the components. The plugin API interacts with the parser and the writer, and the knowing the stages is the correct registration.
The version problem. The v3 and the v4 have the different internal architecture. The v3 used the PostCSS and the JavaScript JIT. The v4 uses the Rust and the Lightning CSS. The migration is the internal change.
The portability problem. The standalone binary, the npm package, and the Vite plugin are the different distributions of the same engine. The internals are the same, and the understanding transfers.
a. The utility scanner
The scanner finds the class candidates in the source files. It walks the files, reads the text, and extracts the strings with the regex.
The file discovery
The scanner starts with the source list. The v4 has the automatic detection: the project root, the .gitignore respect, and the binary ignore. The @source directive adds the explicit paths.
@import "tailwindcss";
@source "../../packages/ui/src/**/*.{ts,tsx}";
@source not "./src/vendor/**/*.js";
The @source adds the path, and the @source not removes it. The auto-detection covers the rest.
The text reading
The scanner reads each file as the plain text. It does not parse the JavaScript or the JSX. The class="...", the className="...", the tw="...", and the other patterns are the recognized.
// The source
<div className="flex items-center bg-blue-600 p-4">...</div>
// The scanner reads the text
// The candidates: flex, items-center, bg-blue-600, p-4
The candidate regex
The scanner extracts the candidates with the regex. The pattern matches the class-like strings and the splits them.
The extracted: "flex items-center bg-blue-600 p-4"
The split: flex, items-center, bg-blue-600, p-4
The scanner does not validate the candidates. It extracts the strings, and the parser does the validation.
The candidate forms
The scanner recognizes the forms: the plain utility, the variant, the arbitrary value, the arbitrary property, the negative, the important, and the stacked variant.
| The form | The example |
|---|---|
| The plain | bg-blue-600 |
| The variant | hover:bg-blue-700 |
| The arbitrary | bg-[#1da1f2] |
| The property | [mask-type:luminance] |
| The negative | -mt-4 |
| The important | bg-red-500! |
| The stacked | dark:md:hover:bg-blue-700 |
The dynamic detection
The scanner can detect the dynamic construction and warn.
// The dynamic
const className = `text-${color}-600`;
// The scanner sees the string but cannot resolve it.
The string is the unrecognizable, and the parser rejects it. The fix is the static form or the CSS variable.
The scanner table
| The element | The purpose |
|---|---|
| The file discovery | The source list |
| The text reading | The plain text |
| The regex | The candidate extraction |
| The split | The class strings |
| The dynamic | The unrecognizable |
b. The class parser
The parser takes the candidate and resolves it into a utility definition. It splits the variants, matches the base, and reads the modifiers.
The candidate splitting
The parser splits the candidate into the variant chain and the utility part.
The candidate: dark:md:hover:bg-blue-700
The variants: dark, md, hover
The utility: bg-blue-700
The split is the colon, and the last part is the utility.
The variant resolution
The parser resolves each variant against the variant registry. The dark is the custom variant, the md is the media query, and the hover is the pseudo-class.
// The variant registry
// dark → &:where(.dark, .dark *)
// md → @media (min-width: 768px)
// hover → &:hover
The parser composes the selectors in the order.
The utility lookup
The parser looks up the utility in the utility registry. The bg-blue-700 is the background-color with the value from the theme.
// The utility registry
// bg-blue → the background-color
// The value: the theme('colors.blue.700')
The lookup is the hash map, and the resolution is the fast.
The arbitrary value parsing
The parser parses the arbitrary value. The bg-[#1da1f2] is the background-color with the inline value.
The candidate: bg-[#1da1f2]
The utility: bg
The value: #1da1f2
The declaration: background-color: #1da1f2
The parser extracts the value and the generates the declaration.
The modifier parsing
The parser parses the modifier. The text-shadow-lg/red is the lg and the red.
The candidate: text-shadow-lg/red
The utility: text-shadow
The value: lg
The modifier: red
The parser passes the both to the function.
The negative parsing
The parser parses the negative. The -mt-4 is the negative of the mt-4.
The candidate: -mt-4
The utility: mt
The negative: true
The value: 4
The declaration: margin-top: calc(var(--spacing-4) * -1)
The important parsing
The parser parses the important. The bg-red-500! is the !important.
The candidate: bg-red-500!
The utility: bg-red-500
The important: true
The declaration: background-color: ... !important
The parser table
| The step | The purpose |
|---|---|
| The split | The variants and the utility |
| The variant resolution | The registry lookup |
| The utility lookup | The registry lookup |
| The arbitrary parsing | The inline value |
| The modifier parsing | The second value |
| The negative parsing | The negative |
| The important parsing | The !important |
c. The CSS writer
The writer takes the resolved definitions and emits the CSS rules. It orders the rules, deduplicates, and writes the output.
The layer order
The writer emits the rules in the layer order: the theme, the base, the components, the utilities.
@layer theme { ... }
@layer base { ... }
@layer components { ... }
@layer utilities { ... }
The order is the precedence, and the utilities are the last.
The rule deduplication
The writer deduplicates the identical rules. The same candidate produces the one rule.
/* The duplicate */
.bg-blue-600 { background-color: ...; }
.bg-blue-600 { background-color: ...; }
/* The deduplicated */
.bg-blue-600 { background-color: ...; }
The deduplication is the hash set of the rules.
The variant composition
The writer composes the variant selectors. The dark:md:hover:bg-blue-700 produces the nested.
@media (min-width: 768px) {
.dark .dark\:md\:hover\:bg-blue-700:hover {
background-color: var(--color-blue-700);
}
}
The composition is the wrapping in the reverse order of the chain.
The escaping
The writer escapes the special characters in the selector. The bg-[#1da1f2] is the .bg-\[\#1da1f2\].
.bg-\[\#1da1f2\] {
background-color: #1da1f2;
}
The escape is the backslash for the CSS selector.
The specificity
The writer manages the specificity. The single-class utility has the (0,1,0), and the variant adds the pseudo-class.
/* The plain */
.bg-blue-600 { ... }
/* The specificity: (0,1,0) */
/* The hover */
.hover\:bg-blue-700:hover { ... }
/* The specificity: (0,2,0) */
The property conflict
The writer resolves the property conflict. The same property in the same layer is the last-wins.
/* The two rules */
.bg-red-500 { background-color: red; }
.bg-blue-500 { background-color: blue; }
/* The order in the CSS determines the winner. */
The order is the deterministic: the utility category and the value.
The output format
The writer produces the minified or the formatted output.
/* The formatted */
.flex {
display: flex;
}
/* The minified */
.flex{display:flex}
The --minify flag controls the format.
The writer table
| The step | The purpose |
|---|---|
| The layer order | The precedence |
| The deduplication | The one rule |
| The variant composition | The nested |
| The escaping | The special |
| The specificity | The management |
| The conflict | The last-wins |
| The output | The minified or the formatted |
Complete Example Session
// ============================================
// PART 1: THE SOURCE
// ============================================
<div className="flex items-center gap-4 bg-blue-600 p-4 text-white">
<h1 className="text-2xl font-bold">Hello</h1>
<button className="rounded-md bg-white px-4 py-2 text-blue-600 hover:bg-blue-50">
Click
</button>
</div>
// ============================================
// PART 2: THE SCANNER
// ============================================
// The scanner reads the file.
// The candidates:
// flex, items-center, gap-4, bg-blue-600, p-4, text-white
// text-2xl, font-bold
// rounded-md, bg-white, px-4, py-2, text-blue-600
// hover:bg-blue-50
// ============================================
// PART 3: THE PARSER — THE PLAIN UTILITY
// ============================================
// The candidate: bg-blue-600
// The utility: bg
// The value: blue-600
// The theme: var(--color-blue-600)
// The declaration: background-color: var(--color-blue-600)
// ============================================
// PART 4: THE PARSER — THE VARIANT
// ============================================
// The candidate: hover:bg-blue-50
// The variants: hover
// The utility: bg-blue-50
// The selector: &:hover
// The declaration: background-color: var(--color-blue-50)
// ============================================
// PART 5: THE PARSER — THE ARBITRARY
// ============================================
// The candidate: bg-[#1da1f2]
// The utility: bg
// The value: #1da1f2
// The declaration: background-color: #1da1f2
// ============================================
// PART 6: THE WRITER — THE LAYER ORDER
// ============================================
@layer theme { ... }
@layer base { ... }
@layer components { ... }
@layer utilities {
.flex { display: flex; }
.items-center { align-items: center; }
.gap-4 { gap: var(--spacing-4); }
.bg-blue-600 { background-color: var(--color-blue-600); }
.p-4 { padding: var(--spacing-4); }
.text-white { color: var(--color-white); }
.text-2xl { font-size: var(--text-2xl); }
.font-bold { font-weight: 700; }
.rounded-md { border-radius: var(--radius-md); }
.bg-white { background-color: var(--color-white); }
.px-4 { padding-inline: var(--spacing-4); }
.py-2 { padding-block: var(--spacing-2); }
.text-blue-600 { color: var(--color-blue-600); }
.hover\:bg-blue-50:hover { background-color: var(--color-blue-50); }
}
// ============================================
// PART 7: THE WRITER — THE VARIANT COMPOSITION
// ============================================
// The candidate: dark:md:hover:bg-blue-700
@media (min-width: 768px) {
.dark .dark\:md\:hover\:bg-blue-700:hover {
background-color: var(--color-blue-700);
}
}
// ============================================
// PART 8: THE WRITER — THE ESCAPING
// ============================================
.bg-\[\#1da1f2\] {
background-color: #1da1f2;
}
// ============================================
// PART 9: THE WRITER — THE DEDUPLICATION
// ============================================
// The duplicate candidate: bg-blue-600
// The one rule is the emitted.
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. The scanner reads the files.
// 2. The regex extracts the candidates.
// 3. The parser splits the variants.
// 4. The parser looks up the utility.
// 5. The parser parses the arbitrary.
// 6. The writer orders the layers.
// 7. The writer deduplicates.
// 8. The writer composes the variants.
// 9. The writer escapes the special.
// 10. The writer produces the output.
The ten parts showed the source, the scanner, the parser for the plain utility, the parser for the variant, the parser for the arbitrary, the writer for the layer order, the writer for the variant composition, the writer for the escaping, the writer for the deduplication, and the complete pattern.
Quick Reference
The Three Stages
| The stage | The purpose |
|---|---|
| The scanner | The candidate extraction |
| The parser | The utility resolution |
| The writer | The CSS emission |
The Scanner
| The element | The purpose |
|---|---|
| The file discovery | The source list |
| The text reading | The plain text |
| The regex | The candidate |
| The dynamic | The unrecognizable |
The Parser
| The step | The purpose |
|---|---|
| The split | The variants and the utility |
| The variant resolution | The registry |
| The utility lookup | The registry |
| The arbitrary | The inline value |
| The modifier | The second value |
| The negative | The negative |
| The important | The !important |
The Writer
| The step | The purpose |
|---|---|
| The layer order | The precedence |
| The deduplication | The one rule |
| The variant composition | The nested |
| The escaping | The special |
| The conflict | The last-wins |
| The output | The minified or the formatted |
The Candidate Forms
| The form | The example |
|---|---|
| The plain | bg-blue-600 |
| The variant | hover:bg-blue-700 |
| The arbitrary | bg-[#1da1f2] |
| The property | [mask-type:luminance] |
| The negative | -mt-4 |
| The important | bg-red-500! |
| The stacked | dark:md:hover:bg-blue-700 |
Best Practices
✅ Do This:
// Use the static class.
<div className="bg-blue-600"> // ✅
/* Use the @source. */
@source "./**/*.go"; // ✅
// Use the arbitrary value.
<div className="bg-[#1da1f2]"> // ✅
// Use the variant.
<button className="hover:bg-blue-700"> // ✅
# Use the --watch to see the rebuild.
./tailwindcss -i input.css -o output.css --watch // ✅
# Use the --minify for the production.
./tailwindcss -i input.css -o output.css --minify // ✅
/* Use the layer order. */
@layer base, components, utilities; // ✅
❌ Don’t Do This:
// Don't construct the class with the template.
const className = `text-${color}-600`; // ❌ The scanner cannot resolve
// Don't rely on the unknown utility.
<div className="my-custom"> // ❌ The parser rejects
/* Don't forget the @source. */
@import "tailwindcss"; // ❌ The no classes
// Don't use the conflicting classes.
<div className="bg-red-500 bg-blue-500"> // ❌ The last-wins
/* Don't use the complex selector for the utility. */
.page .content .card { ... } // ⚠️ The slower
# Don't skip the minify in the production.
./tailwindcss -i input.css -o output.css // ⚠️ The larger
// Don't use the dynamic in the SSR.
const className = getDynamic(); // ❌ The mismatch
Common Pitfalls
| The pitfall | The reason | The fix |
|---|---|---|
| The class not generated | The scanner did not find it | Use the static or the @source |
| The arbitrary ignored | The wrong syntax | Use the [...] |
| The variant not applied | The wrong order | Check the composition |
| The conflict lost | The last-wins | Use the conditional |
| The duplicate rule | The repeated candidate | The writer deduplicates |
| The specificity | The deep selector | Use the single-class |
| The build slow | The large scan | Narrow the @source |
Real-World Examples
1. The Scanner Candidate
<div className="bg-blue-600">
// The candidate: bg-blue-600
2. The Parser Utility
The utility: bg
The value: blue-600
The declaration: background-color: var(--color-blue-600)
3. The Parser Variant
The candidate: hover:bg-blue-700
The variant: hover
The selector: &:hover
4. The Parser Arbitrary
The candidate: bg-[#1da1f2]
The declaration: background-color: #1da1f2
5. The Writer Layer
@layer utilities {
.bg-blue-600 { background-color: ...; }
}
6. The Writer Variant
@media (min-width: 768px) {
.dark .dark\:md\:hover\:bg-blue-700:hover { ... }
}
7. The Writer Escaping
.bg-\[\#1da1f2\] { background-color: #1da1f2; }
8. The Writer Deduplication
.bg-blue-600 { ... }
/* The one rule */
9. The @source
@source "./**/*.go";
10. The Complete Pattern
<div className="bg-blue-600 hover:bg-blue-700">
Visual
The Three Stages
┌────────────────────────────────────────────────────────────┐
│ THE THREE STAGES │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE SCANNER │ │
│ │ The files → the text → the candidates. │ │
│ │ The regex, the split. │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE PARSER │ │
│ │ The candidate → the utility → the declaration. │ │
│ │ The variants, the arbitrary, the modifiers. │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ THE WRITER │ │
│ │ The definitions → the rules → the CSS. │ │
│ │ The layers, the deduplication, the escaping. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Scanner
┌────────────────────────────────────────────────────────────┐
│ THE SCANNER │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The source file │ │
│ │ <div className="flex bg-blue-600 p-4"> │ │
│ │ <button className="hover:bg-blue-700"> │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The candidates │ │
│ │ flex, bg-blue-600, p-4, hover:bg-blue-700 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The scanner does not validate. It extracts. │
└────────────────────────────────────────────────────────────┘
The Parser
┌────────────────────────────────────────────────────────────┐
│ THE PARSER │
│ │
│ THE CANDIDATE │
│ ┌──────────────────────────────────────────────────┐ │
│ │ dark:md:hover:bg-blue-700 │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The variants: dark, md, hover │ │
│ │ The utility: bg-blue-700 │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The declarations │ │
│ │ background-color: var(--color-blue-700) │ │
│ │ The selector: the nested │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Writer
┌────────────────────────────────────────────────────────────┐
│ THE WRITER │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The layer order │ │
│ │ @layer theme { } │ │
│ │ @layer base { } │ │
│ │ @layer components { } │ │
│ │ @layer utilities { } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The rules │ │
│ │ .flex { display: flex; } │ │
│ │ .bg-blue-600 { background-color: ...; } │ │
│ │ .hover\:bg-blue-700:hover { ... } │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The output │ │
│ │ The minified or the formatted │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Variant Composition
┌────────────────────────────────────────────────────────────┐
│ THE VARIANT COMPOSITION │
│ │
│ THE CHAIN │
│ ┌──────────────────────────────────────────────────┐ │
│ │ dark → &:where(.dark, .dark *) │ │
│ │ md → @media (min-width: 768px) │ │
│ │ hover → &:hover │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ v │
│ THE COMPOSED │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @media (min-width: 768px) { │ │
│ │ .dark .dark\:md\:hover\:bg-blue-700:hover { │ │
│ │ background-color: var(--color-blue-700); │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The wrapping is the reverse of the chain. │
└────────────────────────────────────────────────────────────┘
Summary
| The item | The value |
|---|---|
| The scanner | The candidate extraction |
| The parser | The utility resolution |
| The writer | The CSS emission |
The @source | The scan path |
| The regex | The extraction |
| The variant chain | The split |
| The utility registry | The lookup |
| The arbitrary value | The inline |
| The layer order | The precedence |
| The deduplication | The one rule |
Key takeaways:
- The scanner extracts the candidates without the understanding of the Tailwind syntax. It reads the files as the plain text, matches the class-like strings with the regex, and splits them. The dynamic construction is the unrecognizable.
- The parser resolves the candidate into the utility definition. It splits the variant chain, looks up the utility in the registry, and reads the arbitrary value, the modifier, the negative, and the important.
- The writer emits the rules in the layer order. The theme, the base, the components, and the utilities. The utilities are the last, and the precedence is the deterministic.
- The deduplication removes the identical rules. The same candidate produces the one rule, and the writer tracks the emitted rules with the hash set.
- The variant composition wraps the selectors in the reverse order of the chain. The
dark:md:hover:produces the media query with the.darkancestor and the:hover. - The escaping makes the special characters safe. The
bg-[#1da1f2]is the.bg-\[\#1da1f2\], and the backslash is the escape. - The conflict is the last-wins. The two utilities that set the same property are the order in the CSS, and the order is the utility category and the value.
- The
@sourceis the scan path. The auto-detection covers the project, and the@sourceadds the explicit. The@source notremoves it.
Remember: The Tailwind engine is the pipeline of the scanner, the parser, and the writer. The scanner finds the candidates, the parser resolves the utilities, and the writer emits the CSS. The three stages are the separate, and the understanding is the predictability, the performance, and the debugging. The result is the framework where the class in the source becomes the rule in the stylesheet, and the internals explain every step.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!