notes 5 min css
Recovering a Tailwind theme from a site you did not build
Nobody ships tailwind.config.js. What reaches the browser is a compiled stylesheet and a great many class attributes — and between those two, most of a design system is still legible.
This is a normal thing to want. You are taking over a codebase whose design decisions were never written down, or matching a component to a system somebody else owns, or checking whether the site you are about to redesign has a scale at all.
The build is not a black box. It is a set of decisions, flattened — and most of them survive the flattening.
Version 4 hands it to you #
Tailwind 4 moved the theme into CSS. A @theme block compiles to custom properties on the root element, and custom properties are readable at runtime, in order, with their computed values.
const root = getComputedStyle(document.documentElement);
[...document.styleSheets]
.flatMap((s) => [...s.cssRules])
.filter((r) => r.selectorText === ':root')
.flatMap((r) => [...r.style])
.filter((name) => name.startsWith('--color-'))
.map((name) => [name, root.getPropertyValue(name).trim()]);
So on a version 4 site the theme is not inferred at all. It is read — every token the build kept, under the name the author gave it, which is the part that matters:
getComputedStyle(document.documentElement)
- --color-surface
- #ffffff
- --color-rule
- #d5d9e0
- --color-ink
- #0b0d12
- --color-accent-deep
- #0e7490
- --color-danger
- #dc2626
- --color-warn
- #b45309
the names are the system; the values are its output
--color-brand-600 tells you something that #0e7490 does not. The names are the design system. The values are only its output.
The names are also the fastest way to date a codebase. A palette named for its role — surface, rule, ink, danger — is a system somebody maintained. A palette named blue-1 through blue-9 is a palette somebody pasted.
Version 3 has to be inferred #
Before the theme lived in CSS it lived in a config file that never shipped. What ships is the generated utilities — and the scale is recoverable from them, because every generated class is a name-value pair. .text-slate-700 carries the value of slate-700 in its own declaration.
- Collect every rule whose selector is a Tailwind-shaped class.
-
Group by the utility prefix —
text-,bg-,p-,rounded-,shadow-. - Read the declared value out of each rule.
-
Take the breakpoints from the
@mediaqueries the rules are nested inside.
It is arithmetic on a stylesheet rather than analysis. The gap is that a version 3 build only emits what was used: the config may have carried forty greys, and the stylesheet carries the nine somebody wrote a class for.
Reading the scale rather than the colours #
Colour is the part everybody recovers first and the part that matters least — a palette can be lifted from a screenshot. The spacing scale cannot, and it is the thing that makes a new component look like it belongs.
const seen = new Map();
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
for (const side of ['paddingTop', 'marginBottom', 'gap']) {
const v = s[side];
if (v && v !== '0px' && v !== 'normal') {
seen.set(v, (seen.get(v) ?? 0) + 1);
}
}
}
[...seen].sort((a, b) => b[1] - a[1]).slice(0, 12);
The output tells you two things at once. A tidy result — 8px, 16px, 24px, 32px, each used hundreds of times — is a scale somebody kept to. A long tail of 13px, 19px, 27px used twice each is a scale that exists in the config and not in the codebase, which is a different finding and a more actionable one.
- Breakpoints
- Read from the
@mediaqueries in the stylesheet, deduplicated. Faster than any documentation, and correct by construction. - Radii and shadows
- Small, closed sets. Three radii is a system; eleven is a codebase where every component chose for itself.
- Type scale
- The computed
font-sizeandline-heightpairs actually in use. The pairing is the part a config file lists and a page proves.
What is gone for good #
- Anything tree-shaken
- A token defined and never used is indistinguishable from a token that never existed. True in both versions.
- Plugin configuration
- Custom variants, container queries, typography plugin settings. The output is visible; the switch that produced it is not.
- The reasoning
- You can recover that the spacing scale steps by 4 pixels. You cannot recover that the team agreed to it in a meeting, or that step 6 is deliberately absent.
- The names, in version 3
#0e7490is in the stylesheet. Whether the team called itaccent-deeporcyan-700is not — unless the class list happens to say so.
What to do with what you get #
The recovered theme is a starting file, not an answer. Three things are worth doing with it before it is useful:
- Sort each scale and look at the gaps. A colour ramp missing its 400 step, or a spacing scale with two values eleven pixels apart, is where the system was patched rather than extended.
- Count the uses. A token used once is a decision somebody made in a hurry; a token used four hundred times is the system. The count is the difference between a palette and a scale.
- Name what you recovered, if the build could not. On a version 3 site you are handed values without names, and writing the names down is the act that turns a list of hex codes back into a design system.
One honest caution: a recovered theme is evidence about the shipped page and nothing more. It cannot tell you which values were deliberate, and it will happily present an inconsistency as a scale. Treat it as a survey, not as documentation.
The used subset is the useful subset #
This reads like a limitation and mostly is not. If you are matching a component to a site, the values in the shipped stylesheet are exactly the values in use — the recovered theme is the live one, with the aspirational parts already removed.
Where it does matter is auditing, and there it cuts the other way. A recovered scale with eleven greys in it is not proof that eleven greys were designed. It is proof that eleven greys were shipped — which is the more useful of the two findings, and the one no config file would have told you.
Written by Ján Turský
Building LoupeKit and other browser tools out of Bratislava, under Apptiary.
tools in this note