Skip to content We're live on Product Hunt today Upvote
install

notes 5 min css

note

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.

the theme, read off a live page
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

What comes back off a version 4 build: the token, its name, and the value it resolves to. The name is the half a screenshot of the site could never give you.

--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 @media queries 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.

every spacing value the page actually uses, ranked
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 @media queries 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-size and line-height pairs 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
#0e7490 is in the stylesheet. Whether the team called it accent-deep or cyan-700 is 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:

  1. 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.
  2. 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.
  3. 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