Every project I start eventually grows a colors.ts file, a second set of spacing
constants, and a third place where the font sizes live. Tailwind v4 removes the
need for most of that by letting the theme live in CSS, next to the rest of the
stylesheet.
Why tokens belong in CSS
A JavaScript config is invisible to the browser. Anything that needs a theme value at runtime — a canvas chart, a third-party widget, an inline SVG — has to import the config and duplicate the lookup.
Custom properties do not have that problem. Once a token is a CSS variable it is
readable from a stylesheet, from getComputedStyle, and from a style attribute,
which means there is exactly one source of truth.
Mapping Figma variables to @theme
The mapping is mechanical once you agree on a naming scheme. A Figma variable
called color/surface/raised becomes --color-surface-raised, and Tailwind
generates bg-surface-raised from it automatically:
@theme {
--color-surface-raised: #14161a;
--color-accent: #6c5ce7;
--radius-card: 1.25rem;
}
The thing worth being strict about is that no component gets to invent a value. If a design needs a colour that is not in the theme, the theme is wrong, not the component.
Keeping dark mode honest
I define the light values on :root and override them inside a media query. Because
every component only ever references the token, dark mode costs nothing per
component — the override happens once.
The trap is semi-transparent overlays. rgb(0 0 0 / 0.06) reads as a subtle divider
on white and as nothing at all on near-black, so overlays need their own tokens
rather than being computed inline.
What I would do differently
I spent too long trying to express every spacing value as a token. Spacing is the one axis where Tailwind's default scale is already good, and re-deriving it from Figma bought me nothing but a migration.
Colour, radius, shadow, and typography were worth tokenising. Spacing was not.