The framework was evaluated in 2020, 2022 and 2023, and rejected each time for reasons that had become habitual rather than examined. The January release moves configuration out of JavaScript and into CSS, which is a large enough change to the thing being evaluated that the previous answers do not carry.
The symptom
the stylesheet, accounted for honestly:
2,100 lines, of which
412 layout primitives — grid, stack, cluster,
sidebar. written once, changed twice in four
years.
880 components. changed constantly.
290 tokens, generated from tokens.json.
518 utilities — spacing, colour, display,
typography helpers.
people who can change it confidently: 1
people who could change a framework's classes
confidently on day one: 3 of 3The fluency number is the finding. A stylesheet that one person can change is a bottleneck regardless of its quality, and the utility layer — the quarter that a framework replaces — is where most changes land.
Why it happens
A hand-written design system is better tailored and worse shared. Every decision in it is justified and none of them are discoverable, so the cost of a change is proportional to how much of the file somebody has read.
The fix
What actually changed in 4.0
tailwind.config.js → @theme in a stylesheet
content globs → automatic source detection
the PostCSS chain → one plugin, or a Vite plugin
@tailwind base → @import "tailwindcss"
the engine rewrite is the headline and is irrelevant to
a build that already takes 400ms.
the configuration move is the change that matters: the
settings are now readable by anybody who reads CSS,
rather than by anybody who reads a JavaScript object
with a documented shape.The token overlap
/* theirs */
@theme {
--color-brand: #1f6feb;
--spacing-m: 1.5rem;
--font-sans: Inter, sans-serif;
}
/* ours, generated from tokens.json since 2022 */
:root {
--wp--preset--color--brand: #1f6feb;
--wp--preset--spacing--m: 1.5rem;
}
These are the same mechanism with different naming conventions, which means the token half of a migration is a rename in the generator rather than a rewrite. That removes the strongest previous objection — that adopting a framework would fork the design system — and it was the finding that made the rest of the evaluation worth doing.
One component, converted honestly
the product card, both ways:
hand-written 41 lines of CSS, 6 class names in
the markup
framework 0 lines of CSS, 31 utility classes
across 8 elements
output after purging, for the whole site:
ours 18.4 KB gzipped
theirs 11.2 KB gzipped
both (during a
migration) 26.1 KB
and the markup diff is the part to look at rather than
the numbers.Seven kilobytes is a real saving and is not a reason to do anything. The markup is where the trade actually is: thirty-one utility classes on eight elements is readable to somebody who knows the vocabulary and is noise to somebody who does not, and the same is true of six semantic class names in the other direction.
What it would replace, and what it would not
utilities 518 lines. replaced entirely, and
this is the part where a framework
is straightforwardly better.
tokens 290 lines. renamed, not replaced.
components 880 lines. replaced by markup,
which moves the complexity rather
than removing it.
layout
primitives 412 lines. NOT replaced. grid-
template-areas, container queries
and subgrid are not utility-shaped.
so: a quarter is a clear win, a quarter is a rename,
and half is a relocation or an exception.The decision
not adopted, and the reason is not technical:
a migration is 880 lines of components becoming
markup across 140 templates, plus 412 lines that
stay and now sit alongside a different paradigm.
the fluency problem it solves is real. it would be
solved more cheaply by two afternoons of somebody
else reading the stylesheet.
and the utility layer — the clear win — can be
adopted on its own without the framework, which is
what we did: 518 lines regenerated from tokens,
named to match the framework's conventions.Taking the idea and not the dependency is the outcome, and it is the one that felt like a cop-out until the naming convention made the utilities discoverable to somebody who knows the framework. That is most of the fluency benefit for none of the migration, and it is available precisely because the token layers are the same idea.
Verifying it worked
$ npx vite build && ls -la dist/assets/*.css | awk '{print $5}'
18104 # unchanged
$ git log --oneline --since=2025-02-01 -- assets/css/ | wc -l
22
$ git log --format='%an' --since=2025-02-01 -- assets/css/ | sort -u | wc -l
3 # was 1
# the prototype branch
$ git branch -d prototype/tailwind-4
Deleted branch prototype/tailwind-4Three people committing to the stylesheet where there had been one is the outcome that the whole exercise was about, and it came from renaming the utilities rather than from adopting anything. Deleting the prototype branch is the other half — a prototype whose conclusions are recorded is finished, and one that survives becomes an argument in two years.
What this costs
A decision that will be revisited, because the reasons are about this team at this size and all of those change. The measurements are in the decision record specifically so the fifth evaluation is a read rather than a fortnight, which is the only durable output of an exercise like this.
Naming our utilities after somebody else’s convention is also a mild form of the coupling we declined — if their vocabulary changes in a major release, ours either diverges or follows something we are not using. That is a small cost and it is a real one, and it was accepted with the same shrug that this paragraph deserves.