| |

Tailwind CSS 80 🎨 Framework Internals: How the Utility Scanner, Class Parser, and CSS Writer Work Together

⏱️ Estimated read: 14 min | |

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 formThe example
The plainbg-blue-600
The varianthover:bg-blue-700
The arbitrarybg-[#1da1f2]
The property[mask-type:luminance]
The negative-mt-4
The importantbg-red-500!
The stackeddark: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 elementThe purpose
The file discoveryThe source list
The text readingThe plain text
The regexThe candidate extraction
The splitThe class strings
The dynamicThe 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 stepThe purpose
The splitThe variants and the utility
The variant resolutionThe registry lookup
The utility lookupThe registry lookup
The arbitrary parsingThe inline value
The modifier parsingThe second value
The negative parsingThe negative
The important parsingThe !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 stepThe purpose
The layer orderThe precedence
The deduplicationThe one rule
The variant compositionThe nested
The escapingThe special
The specificityThe management
The conflictThe last-wins
The outputThe 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 stageThe purpose
The scannerThe candidate extraction
The parserThe utility resolution
The writerThe CSS emission

The Scanner

The elementThe purpose
The file discoveryThe source list
The text readingThe plain text
The regexThe candidate
The dynamicThe unrecognizable

The Parser

The stepThe purpose
The splitThe variants and the utility
The variant resolutionThe registry
The utility lookupThe registry
The arbitraryThe inline value
The modifierThe second value
The negativeThe negative
The importantThe !important

The Writer

The stepThe purpose
The layer orderThe precedence
The deduplicationThe one rule
The variant compositionThe nested
The escapingThe special
The conflictThe last-wins
The outputThe minified or the formatted

The Candidate Forms

The formThe example
The plainbg-blue-600
The varianthover:bg-blue-700
The arbitrarybg-[#1da1f2]
The property[mask-type:luminance]
The negative-mt-4
The importantbg-red-500!
The stackeddark: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 pitfallThe reasonThe fix
The class not generatedThe scanner did not find itUse the static or the @source
The arbitrary ignoredThe wrong syntaxUse the [...]
The variant not appliedThe wrong orderCheck the composition
The conflict lostThe last-winsUse the conditional
The duplicate ruleThe repeated candidateThe writer deduplicates
The specificityThe deep selectorUse the single-class
The build slowThe large scanNarrow 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 itemThe value
The scannerThe candidate extraction
The parserThe utility resolution
The writerThe CSS emission
The @sourceThe scan path
The regexThe extraction
The variant chainThe split
The utility registryThe lookup
The arbitrary valueThe inline
The layer orderThe precedence
The deduplicationThe 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 .dark ancestor 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 @source is the scan path. The auto-detection covers the project, and the @source adds the explicit. The @source not removes 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.


Was this helpful?


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!