Open-Source Wikis

/

Tailwind CSS

/

Features

/

Canonicalization

tailwindlabs/tailwindcss

Canonicalization

Tailwind has a "canonical" form for every class list — the preferred shorthand the framework recommends. px-4 py-4 should be p-4. border-t-2 border-r-2 border-b-2 border-l-2 should be border-2. [&:has(…)] should be has-[…]. The canonicalization engine encodes those rewrite rules.

Where it lives

File Role
packages/tailwindcss/src/canonicalize-candidates.ts Top-level engine (~3,300 lines).
packages/tailwindcss/src/canonicalize-calc-expressions.ts Specialized normalizer for calc(...) values.
packages/tailwindcss/src/utilities.ts (parts of) Knowledge of which utility "owns" a property — used to reverse-map declarations to candidates.

The engine is exposed three ways:

  1. As designSystem.canonicalizeCandidates(candidates, options) — internal API.
  2. As the tailwindcss canonicalize CLI subcommand (packages/@tailwindcss-cli/src/commands/canonicalize/index.ts).
  3. As a step in @tailwindcss/upgrade's template migration (migrate-canonicalize-candidate.ts).

How it works

graph TD
    Input[List of candidates] --> Group[Group by parsed shape]
    Group --> Combine[Combine into shorthand if possible]
    Combine --> Normalize[Normalize values<br/>spacing, calc, units]
    Normalize --> Order[Reorder for stability]
    Order --> Output[Canonical list]

In detail, the engine:

  1. Parses each candidate into a Candidate (packages/tailwindcss/src/candidate.ts).
  2. Groups candidates that share variants and root family. For example, pt-4 pr-4 pb-4 pl-4 all share variant chain [] and root family "padding".
  3. Folds groups into shorthand using the rules in canonicalize-candidates.ts:
    • Equal pt/pr/pb/plp
    • Equal pt/pbpy; equal pl/prpx
    • Same for m, border, rounded, inset, gap, scroll-m, scroll-p, overflow, overscroll, etc.
  4. Normalizes values within each candidate:
    • Move minus signs in/out of arbitrary values where appropriate (e.g. -mt-[20in]mt-[-20in], ml-[calc(-1*var(--width))]-ml-(--width)).
    • Replace bare values exceeding the default scale with arbitrary values (e.g. w-1234 h-1234size-1234).
    • Collapse calc(var(--spacing)*…) into --spacing(…) where possible.
    • Migrate legacy arbitrary forms to modern ones ([&:has(…)]has-[…]).
    • Prefer non-negative shorthands (-tracking-tightertracking-wider).
    • Replace logical-axis utilities where v4 has new ones (start-fullinset-s-full).
  5. Reorders the result to match the framework's deterministic order.

Why is it 3,300 lines?

Because every CSS-property family has its own combination logic. The padding folding is not the same as the border folding (border has style-and-color and width components), which is not the same as the inset folding (which has a logical-axis dimension). Each family is implemented as its own set of recognizers and rebuilders.

The recent CHANGELOG is dense with canonicalization improvements:

  • "Collapse arbitrary values into shorthand utilities (e.g. px-[1.2rem] py-[1.2rem]p-[1.2rem])" — PR #19837
  • "Collapse border-{t,b}-* into border-y-* …" — PR #19842
  • "Collapse scroll-m{t,b}-* …" — PR #19842
  • "Collapse overflow-{x,y}-* and overscroll-{x,y}-*" — PR #19842
  • "Migrate start-* to inset-s-*" — PR #19849
  • "Migrate overflow-ellipsis to text-ellipsis" — PR #19849
  • "Move minus sign inside/outside arbitrary value" — PR #19858
  • "Improve canonicalization for bare values exceeding default spacing scale" — PR #19809
  • "Migrate arbitrary :has() variants from [&:has(…)] to has-[…]" — PR #19991
  • "Preserve significant _ whitespace in arbitrary values" — PR #19986
  • "Add parentheses when removing whitespace from arbitrary values would hurt readability" — PR #19986
  • "Preserve the original unit in arbitrary values instead of normalizing to base units" — PR #19988

Each entry corresponds to a rewrite rule in canonicalize-candidates.ts.

The canonicalize-calc-expressions.ts helper

calc(...) expressions need their own normalizer because:

  • Whitespace can be either significant or insignificant depending on the operator.
  • Nested var(...) and theme(...) calls need to be preserved exactly.
  • Negative values need to be hoisted out of the expression where possible (so canonicalization can find them).

canonicalizeCalcExpression(input) returns the simplified form. It's a small recursive descent parser plus a normalizer.

Tests

canonicalize-candidates.test.ts is ~1,400 lines of input/output pairs. New canonicalization rules typically come with a paragraph of tests; the CHANGELOG-to-tests ratio is roughly 1:N, where N depends on how many edge cases the rule has.

Use from the CLI

# Canonicalize a single class list
echo 'pt-4 pr-4 pb-4 pl-4' | tailwindcss canonicalize
# Output: p-4

# Canonicalize all class lists in a file via the upgrade tool
npx @tailwindcss/upgrade

The CLI subcommand is implemented in packages/@tailwindcss-cli/src/commands/canonicalize/index.ts (~270 lines).

Use from @tailwindcss/upgrade

The upgrade tool's template migration runs canonicalizeCandidates(...) on every safe-to-migrate class list (is-safe-migration.ts decides what's safe). This is how the upgrade output picks up every framework-recommended shorthand.

For more about the upgrade tool see @tailwindcss/upgrade. For the candidate parser the engine relies on see Utilities and the parseCandidate reference in packages/tailwindcss/src/candidate.ts.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Canonicalization – Tailwind CSS wiki | Factory