CSS Theme Variables Generator
Generate a design-token stylesheet with 10-step colour scales, a type scale, spacing, radii and shadows — the foundations of a consistent design system.
A custom property is not a variable
It is a value that gets looked up again every time it is used, somewhere else, by something that inherited it. That distinction is the whole reason theming works, and it is easiest to see in three lines:
| Rule | Declares |
|---|---|
:root | --brand: blue; --accent: var(--brand); |
section | --brand: red; |
--accent is declared once. Read at the root it is
blue. Read inside the section it is
red — and the section never mentions --accent at all.
The var() is not resolved where it is written; it is resolved where it is
used, against whatever --brand is in scope at that point. That is why
overriding one token on a subtree retints everything derived from it, and why a
preprocessor variable cannot do the same job — a Sass variable is resolved once at
build time and has exactly one answer forever.
A property with no var() in it behaves the way you would expect: the same
everywhere, whatever the scope. So the effect above comes from the indirection, not from
scoping on its own.
Fallbacks nest, and that is the pattern worth using
| Written | Resolves to |
|---|---|
var(--a) | nothing — the declaration is dropped |
var(--a, orange) | orange |
var(--a, var(--b, purple)) | purple |
var(--a, var(--c, purple)) | green |
The last two rows are the useful shape: a chain of preferences ending in a literal, so a
component can accept a caller's override, fall back to a theme token, and fall back again to
something safe. Here --c is defined as green, so the chain stops there rather
than reaching purple.
The first row is the one to watch. A var() that produces nothing does not leave
the property at its previous value and does not fall back to the browser default — the
whole declaration is dropped, and the element gets whatever it would have inherited. That
failure is silent, and it looks like a specificity problem when it is not.
A cycle produces nothing, and nothing is not an error
Two properties pointing at each other is not something you will see reported. Both simply produce nothing:
| Property | Resolves to | Why |
|---|---|---|
--a | nothing | cycle |
--b | nothing | cycle |
--ok | teal | literal |
Properties outside the cycle carry on working, which is exactly what makes this hard to spot — most of the sheet still behaves. A property that refers to itself is the same case with a cycle of length one, and a chain three deep is caught the same way.
Here is the part that went the opposite way to my expectation. A fallback rescues a cyclic
reference just as readily as an undefined one:
var(--undefined, silver) gives silver, and
var(--a, silver) with --a caught in a cycle also gives
silver. Both end in the same state — the property produces nothing —
and producing nothing is precisely what triggers a fallback.
So a cycle is not a distinct kind of error with distinct handling. It is one more way to end up with no value, which is a useful thing to know when debugging: if a token has a fallback, a cycle in it will not announce itself at all, and the only symptom is that the fallback is always the one you get.
How to use
- Pick your base colours.
- Choose a type scale ratio and base size.
- Review the generated tokens.
- Copy the stylesheet as custom properties.
Frequently asked questions
What is a design token?
A named value standing for a design decision — a colour, a spacing step, a radius — used everywhere instead of the raw number. The point is that changing the decision means changing one definition rather than hunting through a stylesheet, and that everything stays consistent by construction.
Why ten steps in a colour scale?
Because it covers the range of uses an interface needs — page background, subtle surface, border, muted text, body text, and the hover and active states of each — without forcing anyone to invent an eleventh. The specific number is convention rather than necessity, but a consistent count across every hue is what makes the system work.
What is a type scale ratio?
A constant multiplier between successive font sizes, so sizes relate to each other rather than being picked arbitrarily. A ratio around 1.25 gives a gentle progression suited to dense interfaces, and 1.5 or higher gives dramatic contrast suited to editorial layouts.
Why use a spacing scale rather than arbitrary values?
Because arbitrary spacing is where visual consistency quietly dies. A scale — often based on multiples of four or eight — means every gap in the interface relates to every other, and it removes an entire category of decision from day-to-day work.
How do I handle dark mode with tokens?
By redefining the token values under a dark-mode selector or media query, while every component continues to reference the same names. Components should never know which theme is active, which is the main practical argument for tokens over hard-coded colours.
Do generated tokens meet contrast requirements?
Not automatically, and the generator cannot know which pairings you intend. A scale gives you related colours; whether step four on step nine passes for body text has to be checked. Build the scale, then audit the pairings you actually use.
🔒 This tool runs entirely in your browser. Nothing you enter is uploaded, logged, or stored.