The state sat in the middle of the name
…accent-primary-hover in CSS, …/Hover/background in Figma.
Our design tokens lived in three places: Figma, CSS and JSON. Each had its own naming rules and was maintained by hand, so they slowly drifted apart. I defined a single naming grammar. Now every token has one logical name, and the Figma path, the CSS property and the JSON key all follow from it mechanically.
The same token existed in Figma, in the CSS files and in the JSON exported for code. Each source spelled it differently, and nothing kept them in sync except people.
--color-background-accent-primary-hoverCOLOR/Accent/Primary accent/Hover/backgroundCOLOR.Accent.Primary accent.Hover.backgroundbrand-500 in CSS and to secondary-600 in Figma. Designers and developers were shipping different blues.colore-border-selected lived on as a real token name. Developers read these names and generate code from them.Instead of fixing names one by one, I proposed a grammar. Every token is described by the same five segments, always in the same order. Once the grammar exists, naming stops being a matter of taste and becomes something a reviewer can check.
In code the tier is usually implied by the file a token lives in, so the semantic token above reads --color-primary-background-hover.
Each rule in the grammar exists because of a problem we actually had. This is the part I’d point to in a review: not the names themselves, but the reason behind each one.
…accent-primary-hover in CSS, …/Hover/background in Figma.
The state is always the last segment, and appears once.
All states of one element sort together, and a developer can find the hover variant without guessing.
COLOR, Accent, SIZING & SPACING.
Every segment is lowercase. No exceptions.
Names translate to code without manual fixes. All 438 Figma variables have already been converted.
A semantic token called brand tied a role back to a raw colour.
Semantic roles describe meaning: primary, not brand; destructive, not negative-500.
If the primary colour moves to another palette, no semantic name has to change.
The system grew without anyone deciding it should.
A component token exists only when the component diverges from the semantic value.
The system stays small. Each component token now signals a deliberate exception.
The same variable was used for a fill and for a stroke.
One name per property: …background… and …border… are separate tokens, even if they alias the same value.
Fill and stroke can change independently later without a rename.
Figma, CSS and JSON each needed their own update.
One logical name. The three formats are derived from it mechanically.
Figma’s code syntax is set to var(--token), so Dev Mode shows the exact CSS name.
Deleting a variable and creating a new one detaches every instance that used it.
Rename, never recreate. The variable keeps its ID.
The migration runs on a live library without breaking a single component.
The translation is a rule, not a judgement call. Anyone on the team can produce the CSS or JSON name from the Figma path, and a script could do it too.
| Format | Rule | Example |
|---|---|---|
| Figma variable | Segments separated by “/” | color/interactive/primary/background/hover |
| CSS custom property | Kebab-case with a “--” prefix | --color-interactive-primary-background-hover |
| JSON (Tokens Studio) | Segments separated by “.” | color.interactive.primary.background.hover |
Tokens live in their own files (core-tokens.css, semantic-tokens.css, component-tokens.css), never inside the components. Component classes stay as they are. The class says what to style; the token says with which value.
It uses exactly the semantic colour, so it references the semantic token directly.
.button--primary:hover {
background: var(--color-interactive-primary-background-hover);
}
It needs a softer colour than the semantic default. It diverges, so it earns its own token.
.badge[data-state="selected"] {
background: var(--badge-background-selected);
}
The checkbox was the first component we migrated. Its six variables show almost every problem in one place.
| # | Before | After (Figma path) | Tier | |
|---|---|---|---|---|
| 1 | color/accent/secondary/default/default (fill) | → | color/secondary/background/default | semantic |
| 1b | same variable, used as stroke | → | color/secondary/border/default | semantic |
| 2 | color/accent/secondary/default/icon | → | color/secondary/foreground/default | semantic |
| 3 | color/accent/disable state/disabled/disabled | → | color/secondary/background/disabled | semantic |
| 4 | color/surface/default | → | color/neutral/background/default | semantic |
| 5 | borders & radius/radius-extrasmall | → | core/border/radius/extrasmall | core (was semantic) |
| 6 | sizing & spacing/border size/border-size-medium | → | core/border/size/medium | core |
neutral.neutral role and the fill/stroke split for the checkbox.A design system doesn’t break in its components. It breaks in its names, long before anyone notices.