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:
- As
designSystem.canonicalizeCandidates(candidates, options)— internal API. - As the
tailwindcss canonicalizeCLI subcommand (packages/@tailwindcss-cli/src/commands/canonicalize/index.ts). - 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:
- Parses each candidate into a
Candidate(packages/tailwindcss/src/candidate.ts). - Groups candidates that share variants and root family. For example,
pt-4 pr-4 pb-4 pl-4all share variant chain[]and root family "padding". - Folds groups into shorthand using the rules in
canonicalize-candidates.ts:- Equal
pt/pr/pb/pl→p - Equal
pt/pb→py; equalpl/pr→px - Same for
m,border,rounded,inset,gap,scroll-m,scroll-p,overflow,overscroll, etc.
- Equal
- 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-1234→size-1234). - Collapse
calc(var(--spacing)*…)into--spacing(…)where possible. - Migrate legacy arbitrary forms to modern ones (
[&:has(…)]→has-[…]). - Prefer non-negative shorthands (
-tracking-tighter→tracking-wider). - Replace logical-axis utilities where v4 has new ones (
start-full→inset-s-full).
- Move minus signs in/out of arbitrary values where appropriate (e.g.
- 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}-*intoborder-y-*…" — PR #19842 - "Collapse
scroll-m{t,b}-*…" — PR #19842 - "Collapse
overflow-{x,y}-*andoverscroll-{x,y}-*" — PR #19842 - "Migrate
start-*toinset-s-*" — PR #19849 - "Migrate
overflow-ellipsistotext-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(…)]tohas-[…]" — 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(...)andtheme(...)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/upgradeThe 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.