Invisible Technologies: A Design System Token Architecture on Tailwind and shadcn

A token foundation for a Tailwind and shadcn design system, built in Figma and kept in sync with code through Claude Code, so design and development speak the same language.

Invisible Technologies: A Design System Token Architecture on Tailwind and shadcn
Client
Invisible Technologies
Year
2026
Discipline
Design System, Design Tokens

A design token is a decision, recorded once, in a form both people and machines can read.

Overview

Invisible Technologies' product UI runs on Tailwind and shadcn, but its design tokens were a thin, hand maintained layer with no primitive or semantic structure. We rebuilt the token foundation in Figma and wired it to code, so the system has a real architecture that stays in sync and reads the same in design and development. The product is dark first, which is why the colour ramps and type sheets here are shown in dark; every semantic role still resolves in both modes.

Objectives

  • A two layer model: raw primitives and property scoped semantic roles
  • Tokens native to Tailwind v4 and shadcn compatible
  • Figma as the single source of truth, mirrored to code automatically
  • Zero drift between what designers name and what developers write

The Problem

The tokens were an ad hoc bridge. Hand written CSS variables mapped to shadcn names, which mapped to Tailwind, with no separation of raw values from roles and dimensions hardcoded across components. Adding a color or retuning a scale meant editing many files, and design and code drifted apart.

Approach: Figma inside Claude Code

The entire build ran inside Claude Code connected to Figma over MCP. Claude read and wrote Figma variables directly, generated the token pipeline, and checked every decision against live Tailwind and shadcn documentation. Each design choice and its code output stayed in one loop.

Two Layers: Primitives to Semantic

Primitives are the raw, hidden palette and scales that developers never touch. Semantic tokens are property scoped roles that alias them, covering background, text, border, icon and focus ring in both light and dark. Teams use the roles, never the raw values.

The split as Figma sees it: one hidden primitives collection holding color, typography and the numeric scaler, and separate published collections for the roles that alias into it
Semantic text roles, each one an alias into the hidden primitive layer, resolved side by side for light and dark

Underneath them sits the palette itself, and two decisions shaped it: the colour space the ramps are generated in, and how many rungs each one gets.

Why OKLCH

Hex and HSL make a ramp a matter of taste. Equal numeric steps do not land as equal visual steps, and hue drifts as a colour darkens, so a ramp built by eye bunches up in places and jumps in others. OKLCH puts lightness on a perceptual axis, which means one lightness ladder can carry every hue in the system and contrast behaves the same way wherever a token is applied.

Generating that in a colour picker is guesswork, so I built Rampas, a small tool for producing OKLCH ramps against a fixed lightness ladder. Every primitive here came out of it, which is also why the palette is reproducible: it is a function of its inputs rather than a set of values someone once chose and everyone since has matched by hand.

How Many Steps a Ramp Needs

Tailwind ships eleven steps per colour, 50 through 950. That is generous for light mode, where surfaces sit at the pale end and two or three tints usually cover a whole interface.

Dark mode is where it runs out. A layered surface structure, page behind card behind raised card behind popover, each with a border that has to separate them, needs several distinct near blacks. Tailwind offers 900, 950, and then black. The rest get invented inline, which is the exact thing tokens exist to prevent.

So the primitives here run eighteen steps, and the neutral runs twenty six, from pure white at 0 to pure black at 1000. Seven of those rungs sit at or below gray/800: 800, 850, 900, 925, 950, 975 and 1000. That is the range a dark interface actually builds its surfaces out of.

Gray, twenty six rungs from white at 0 to black at 1000, fine enough at the dark end for surfaces and borders to layer without collapsing into each other
Electric blue, eighteen steps on the same OKLCH lightness ladder, with the brand value sitting at 500

The type scale follows the same logic. Font families, weights and sizes live as primitives, and each role composes them into a single class.

Display roles, where family, size, line height, tracking and weight resolve into one utility

The Core Problem, and the @utility Solution

Tailwind v4's --color-* namespace generates every property from one value, which breaks a property scoped system where primary is one color as a background, another as text, and another as a border. We pivoted to a custom @utility per role, emitting bg-primary, text-primary and border-primary from separate values. This was the key move that let a semantic system live natively in Tailwind.

Dimension Tokens

One hidden numeric scale, the scaler, feeds every dimension family through aliases: spacing, sizing, radius, border width and control height. Unlike color these are not property scoped, so they emit as native Tailwind @theme and let Tailwind generate the utilities itself. A single spacing token produces around eighty five padding, margin, gap and sizing classes on its own.

A sixteen step spacing scale, named by intent rather than by pixel value
Sizing shares the same scaler, so a step means the same thing wherever it appears
Radius mirrors Tailwind's own t-shirt vocabulary
Border width maps to Tailwind's numeric scale, so nothing needs to be emitted for it

No Drift Between Design and Code

Every Figma token name matches its Tailwind class. radius/md is rounded-md, width/2 is border-2, and the sm control height carries the code syntax h-9. Designer and developer share one vocabulary, and Dev Mode shows the exact class next to each token.

The Pipeline

Figma variables are mirrored into a versioned DTCG tokens.json by build scripts, then a generator emits Tailwind CSS, DTCG, flat JSON and plain CSS, with a validator enforcing the locked rules on every change. Change a token in Figma, re sync, and the code regenerates.

Each family takes a different route out of the generator, and that routing is the architecture in one view:

Figma tokenHow it is emittedTailwind class
gray/500:root variable, held out of @themeinternal, never written by hand
background/primary/defaultone @utility per rolebg-primary
text/on-primaryone @utility per roletext-on-primary
body/lg/emphasisone complete @utility per roletext-body-lg-emphasis
spacing/mdnative @theme as --spacing-*p-md, gap-md
radius/mdnative @theme as --radius-*rounded-md
sizing/mdnothing, it reads --spacing-*w-md, h-md
width/2nothing, the value is already nativeborder-2
height/smnothing, carried as code syntaxh-9

Only color and typography need a custom utility, because only they are property scoped. Everything else lives in a real Tailwind namespace, or needs no output at all because our name is already Tailwind's name.

Living Documentation

Every token family has a showcase sheet in Figma that renders the tokens in use, from color ramps and radius swatches to spacing bars and control heights, with the exact class beside each one. The documentation and the specification are the same artifact.

The type playground sets each role in real copy rather than specimen rows, so a decision can be judged where it will actually be read

Challenges

The hard part was never picking colors. It was making the names survive contact with Tailwind. A semantic layer wants to read like intent, primary, subtle, on-primary, but every one of those names also has to become a class that Tailwind will generate and does not already own.

Three collisions set the rules. background/primary/default drops its trailing default and becomes bg-primary, yet text/default and border/default have to keep theirs, because a bare text clashes with Tailwind's own text utilities and a bare border is already its border width utility. Foregrounds went through two rejected schemes before landing: a -foreground suffix that only repeated what the text/ group already said, and a mid-string primary-on-subtle that left "on" ambiguous. We settled on on- as a prefix, so it always reads one way. In typography a size step is always Default and never Medium, so a size can never be mistaken for a weight inside a composite class.

@utility is what made the semantic layer expressible at all, and we took its cost openly. A custom utility does not get Tailwind's /opacity modifier unless it is authored with the --value() syntax, so transparency lives in explicit alpha tokens instead. Every one of these was settled against what the framework actually does, checked with live builds rather than preference.

Conclusion

The system now has a real spine: a hidden primitive scale, property scoped semantic roles, and an honest two way mapping to Tailwind and shadcn. Designers pick meaningful tokens, developers write native classes, and the two never disagree. The foundation is documented, versioned, and ready for the component library.

Design System, Design Tokens, Tailwind CSS, shadcn/ui, Figma, Claude Code

Previous

Takko Fashion Filtering Redesign

Next

Plainify: AI-Ready Product Specifications

Working on something similar? Tell me about it.

→ Get in touch