Design System Components Start With Tokens: Best Practices for Figma Variables
A design system has five core components: design tokens, UI components, patterns, documentation and governance. Tokens come first because everything else consumes them. A button's fill, a dialog's corner radius and a table's row spacing are all token references, so the quality of your token architecture sets the ceiling for everything built on top of it. In Figma, tokens live as variables. In code, they ship as a JSON file in the Design Tokens Community Group (DTCG) format and build into CSS custom properties, Swift or Compose.
This guide is written for teams that already work with variables and want a structure that holds up across brands, themes and a real codebase. It covers what goes into a design system, how to tier your tokens, how to handle theming in Figma, and what designers, developers and the people funding the work each need to get right.
What goes into a design system
If you already run a system, treat this as the inventory to audit against. Each part is defined by what it owns and what it consumes.
Design tokens: the contract between design and code
A token is a named design decision: a color role, a spacing step, a type size. The name is the API. Designers bind layers to it, developers import it, and changing its value should update every surface that uses it without anyone touching those surfaces. When a token name means one thing in Figma and another in code, the contract is broken, and every handoff turns into a translation exercise.
UI components: properties bound to tokens, never to raw values
Every fill, stroke, radius, gap and text property in a component should resolve to a semantic or component token. A hex value inside a component is a fork waiting to happen: the next brand or theme change will skip it, and nobody will notice until a screenshot comes back wrong.
Patterns
Patterns describe how components combine for a task: data entry, filtering, bulk actions, empty and error states. They rarely need tokens of their own. When a pattern starts asking for its own colors, that usually means a semantic role is missing one level down.
Documentation
Good documentation says what each token is for and what it is not for, and it records deprecations along with their replacements. The DTCG format has $description and $deprecated fields for exactly this, so the documentation can travel with the token instead of living in a wiki that drifts away from the file.
Governance
Governance answers three questions: who can change a token, how a change is reviewed, and how it reaches production. Without answers, tokens multiply. Two people add color/surface/raised and color/surface/elevated in the same month, and the semantic layer stops meaning anything.
What stays out of tokens
Not everything visual belongs in the token system. Illustrations, marketing art and game artwork exported as PNGs carry their color inside the image. Tokenizing their palettes adds variables nobody can bind and clutters every picker. Keep tokens for values that layers and code actually reference, and manage artwork as assets.
Design tokens in three tiers
Most mature systems settle on three tiers. The value of the structure comes from one rule: each tier references only the tier directly below it.
Primitives
Primitives hold raw values and nothing else.
| Token | Type | Value |
|---|---|---|
| blue/600 | Color | #006BD9 |
| neutral/50 | Color | #F7F7F7 |
| space/4x | Dimension | 16px |
Give each brand palette its own collection with a single mode. Primitives answer "what values exist?" and should never be bound to a layer directly. Hide them from every picker (more on scopes below) so that designers can't reach them by accident.
Semantic tokens
Semantic tokens name a role, not a value.
| Role | Token |
|---|---|
| Primary action fill | color/action/primary |
| Text and icons on a primary action | color/action/on-primary |
| Default surface | color/surface/default |
| Secondary text | color/text/secondary |
| Danger status | color/status/danger |
Every value is an alias to a primitive. Brands and themes are modes of this collection, so switching a screen from one brand to another means switching a mode, not rebinding layers.
Pair every background role with its foreground role.
| Background | Foreground |
|---|---|
| surface | on-surface |
| action/primary | action/on-primary |
Pairs make contrast checkable. For WCAG 2.2 level AA you need at least 4.5:1 for normal text and 3:1 for large text, and every pair has to pass in every mode, not just the default one.
Component tokens
Component tokens scope a semantic decision to a single component: button/primary/background, dialog/border. They alias semantic tokens, never primitives. Use them when a component genuinely needs to diverge from the semantic default in some brand or state. Don't create one for every property of every component: a component token that always equals its semantic parent is noise you'll have to maintain.
Here is the chain in a DTCG token file. This is an illustration with example values, not code from a real product. The file is data, not application code: a build tool turns it into CSS, Swift or Compose, and you'll find the generated CSS in the developer section below.
{
"blue": {
"600": { "$type": "color", "$value": { "colorSpace": "srgb", "components": [0.0, 0.42, 0.85] } }
},
"neutral": {
"0": { "$type": "color", "$value": { "colorSpace": "srgb", "components": [1.0, 1.0, 1.0] } }
},
"color": {
"action": {
"primary": {
"$type": "color",
"$value": "{blue.600}",
"$description": "Primary action fill. Pair with color.action.on-primary."
},
"on-primary": {
"$type": "color",
"$value": "{neutral.0}",
"$description": "Text and icons on color.action.primary."
}
}
},
"button": {
"primary": {
"background": { "$type": "color", "$value": "{color.action.primary}" },
"foreground": { "$type": "color", "$value": "{color.action.on-primary}" }
}
}
}
Theming architecture: modes, collections or extended collections
Multi-brand and multi-theme setups in Figma come down to three structures. Each one carries real trade-offs.
One semantic collection, one mode per brand or theme. One set of names, and brands switch by mode. The limit is how many modes your plan allows. Figma's plan page lists up to 10 modes per collection on Professional and up to 20 on Organization, while Enterprise gets unlimited modes through extended collections. Brand times theme adds up fast: five brands in light and dark already use the full 10 on Professional.
Separate collections per axis. Put brand in one collection and theme or density in another, and set the mode for each collection independently. This keeps the mode count per collection low, but the axes have to be truly independent. If a dark theme needs different values for each brand, you are back to brand times theme, just spread across more collections.
Extended collections (Enterprise only). A core team publishes a parent collection, and each brand extends it. The extension inherits all modes and variables from its parent and overrides only the values that differ. Of the three structures, this one maps most cleanly to how multi-brand systems are actually owned.
Whichever structure you choose, set modes explicitly. Objects with variables default to Auto, which takes the mode of the parent container and keeps walking up the hierarchy. When nothing above sets a mode, the collection's default applies, which is the left-most mode column. A brand screen that never had its mode set will quietly render in the default brand. Set modes on the top-level frame for every collection the screen uses.
Modes are also a cheap way to test palette variations. Add the candidates as extra modes, switch real screens between them, and delete the losers once a direction is chosen.
On the code side, the DTCG 2025.10 release includes a Resolver module for the same problem. It describes token sets, plus modifiers with named contexts (a theme modifier with light and dark contexts, for example), applied in a declared resolution order where later entries override earlier ones. If your Figma modes and your resolver contexts line up one to one, a theme change means the same thing in both places.
Best practices for designers working in Figma
Scope every variable, and hide primitives. Scopes control which pickers show a variable: fills, text, strokes, gap, corner radius and so on. A text color shouldn't show up when someone binds a frame fill. Know the limit, though: scopes only filter what Figma's pickers show. They don't stop a variable from being bound some other way, for example through a plugin. Scoping is about clarity, so you still need audits. Scoping also improves Check designs: Figma's guidance is to scope precisely, including hiding primitives so they don't surface as suggestions.
Name tokens by purpose, using a taxonomy. Nathan Curtis groups the parts of a token name into four families: namespace (system, theme, domain), object (component, element, component group), base (category, concept, property) and modifier (variant, state, scale, mode). You don't need every level. Pick the levels your system needs, put modifiers last, and use the same order everywhere. Use one case convention (kebab-case is the safest bet for CSS output) and one spelling convention.
One kind of thing per collection. Corner radius and border width are different decisions, so they get different collections or at least different groups. Content strings and booleans that drive prototypes are configuration, not design tokens. Keep them out of the collections you export to code.
Build the spacing scale on a base unit. A 4px base with multiplier names (space/1x, space/2x, space/4x) keeps sizes on the grid, and the names survive a change to the base. Spacing rarely varies by brand, so a single-mode collection shared across brands is usually enough.
Bind typography per role. Variables can drive font family, weight, size, line height and letter spacing. Bind them through text styles so that a heading role changes in one place per brand, rather than on every text layer.
Rename freely, delete carefully. Bindings point to a variable's ID, not its name, and in our work a rename has kept every binding intact. A delete doesn't. The file can keep references to a variable that no longer exists. Rebind the dependents first, then delete.
Run Check designs before handoff. On Organization and Enterprise plans (and Education and Government), Check designs flags hardcoded colors, text styles, corner radius and spacing that could use variables or styles, and suggests replacements from your libraries. Figma says it works best with a published, variables-based system, and precise scoping improves its suggestions.
Best practices for developers
Know the DTCG file format. In the 2025.10 Format module, an object with a $value is a token.
$typeis optional on the token: the type comes from the token's own$type, then from the closest parent group, and an alias takes the resolved type of the token it references.- Tools must not guess a type from the value.
- Curly-brace references (
{color.action.primary}) can only target complete tokens, while$refJSON Pointers can target any property inside a value. $extensionsholds vendor data, and tools must keep extension data they don't understand.$deprecatedtakestrueor a string explaining the deprecation.
Watch the dimension mismatch. DTCG dimensions are objects with a numeric value and a unit of px or rem, and the unit is required even when the value is 0. Figma imports a dimension as a Number in px only. If your code scale is in rem, decide where the conversion lives (usually a transform in the build) and write that decision down.
Know what Figma adds. Figma's import accepts String even though it isn't an official DTCG type. A boolean arrives as a Number with com.figma.type: "boolean" in $extensions, and an alias to a variable in another collection is written through com.figma.aliasData. You can export a single mode, or all modes of a collection, as JSON. Commit those files to the repository and treat them as the source of record for the build.
Keep the alias chain in the output. Style Dictionary's outputReferences option keeps references in the generated files instead of resolving them to raw values. Your CSS then carries the same tiers as the Figma file, and theming becomes a swap of one block of custom properties at runtime.
Here is the generated CSS for the token file above, with a second brand added (example values again). Raw values appear in exactly one place, the primitives block. Every other line is a reference, so no raw value bleeds into the semantic tokens, the component tokens or the component itself:
/* tokens.css, generated with outputReferences enabled */
/* Primitives: the only place raw values live */
:root {
--blue-600: #006bd9;
--teal-700: #00796b;
--neutral-0: #ffffff;
}
/* Semantic tokens: one block per brand, references only */
:root[data-brand="a"] {
--color-action-primary: var(--blue-600);
--color-action-on-primary: var(--neutral-0);
}
:root[data-brand="b"] {
--color-action-primary: var(--teal-700);
--color-action-on-primary: var(--neutral-0);
}
/* Component tokens: reference semantic tokens only */
:root {
--button-primary-background: var(--color-action-primary);
--button-primary-foreground: var(--color-action-on-primary);
}
/* button.css: the component reads component tokens only */
.button-primary {
background: var(--button-primary-background);
color: var(--button-primary-foreground);
}
Switching data-brand on the root element rebrands every button without touching button.css. A hex value anywhere outside the primitives block is a bug, and it's easy to catch in a lint step.
The same tokens in Tailwind, React and Angular. Each framework reads the generated tokens.css. None of them holds a raw value.
In Tailwind CSS v4, clear the default color palette so nobody can reach for bg-blue-500, then expose the component tokens as theme colors. Use @theme inline so the utilities point at the token variable itself and resolve where they're used:
/* app.css */
@import "tailwindcss";
@import "./tokens.css";
@theme {
--color-*: initial;
}
@theme inline {
--color-button-primary: var(--button-primary-background);
--color-on-button-primary: var(--button-primary-foreground);
}
Tailwind then generates utilities that contain references only:
.bg-button-primary { background-color: var(--button-primary-background); }
.text-on-button-primary { color: var(--button-primary-foreground); }
A React component uses those utilities, and switching brands is a single attribute on the root element:
// Button.tsx
import type { ButtonHTMLAttributes } from "react";
export function Button({ className = "", ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
className={`bg-button-primary text-on-button-primary ${className}`}
{...props}
/>
);
}
export function setBrand(brand: "a" | "b") {
document.documentElement.dataset.brand = brand;
}
An Angular component reads the same component tokens straight from its own styles:
// button.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-button',
template: `<button class="button-primary" [type]="type()"><ng-content></ng-content></button>`,
styles: `
.button-primary {
background: var(--button-primary-background);
color: var(--button-primary-foreground);
}
`,
})
export class ButtonComponent {
readonly type = input<'button' | 'submit'>('button');
}
Set code syntax on every variable. Code syntax gives each variable a valid code name per platform (Web, Android, iOS), and Dev Mode shows it in the code snippets when someone inspects a layer that uses the variable. Set the Web value to the exact custom property your build emits. Figma's own team reports that linking code syntax cut down on questions about which token to use, because Dev Mode now hands developers the right var(...) directly.
Strip design-only names in the build. Design files collect working prefixes: version markers, migration groups, experiments. Strip them with a script before the build, and keep that mapping in the repository, not in someone's head.
Plan around the API limit. The Figma REST API for variables is Enterprise only, for reading and writing. On other plans, use the native JSON export, a plugin, or the Figma MCP server (its desktop server needs a Dev or Full seat on a paid plan), and decide which one is the official path so the team doesn't end up with three.
Version, deprecate, then delete. Publish tokens as a versioned package. Mark a token $deprecated with its replacement, keep it for at least one release, then remove it. Lint in CI for the two errors that break the tiers: raw values inside components, and component tokens that alias primitives.
What clients should know before funding a token system
This part is for product and engineering leaders deciding whether the work pays off.
What it buys. A rebrand or a white-label client becomes a change of values, not a redesign. Several brands can run on one component library. Design and code mismatches drop, because both sides use the same names. Accessibility is checked once per color pair per mode instead of screen by screen.
What it costs. Structure work up front, before it shows on any screen. Ongoing governance, because tokens without an owner multiply. And possibly a Figma plan change: mode limits, the variables REST API, extended collections and Check designs all depend on the plan.
Questions to ask a design partner.
- Is there a semantic layer between the brand palettes and the components?
- Do the Figma names match the code names, and who keeps them in sync?
- How does a token change reach production, and how long does that take?
- Which Figma plan does the setup assume?
- How are deprecated tokens retired?
How to build a design system, step by step
- Audit what exists. Inventory every color, type style, spacing value and radius in production and in the design files, and count the variants. The audit tells you how many primitives you really need, and it's usually fewer than the file suggests.
- Define primitives. One collection per brand palette, raw values only, hidden from pickers.
- Define semantic roles. Start with action, surface, text, border and status. Add a role only when a real screen needs it.
- Build components against semantic tokens. Build what the product uses now, not what it might need.
- Add component tokens where a brand or state needs to diverge. Not before.
- Document as you go. Write a description for each token, usage rules for each component, and a deprecation note for anything retired.
- Pilot on a real feature. One shipped feature exposes more gaps than a month of review.
- Wire the pipeline. Export, build, publish a versioned package, and lint in CI.
- Set governance. Name an owner, a review path and a release rhythm.
How design token setups mature
Token setups tend to move through five stages.
- Hardcoded: values typed into every layer.
- Shared styles: a library of color and text styles, still with no shared names in code.
- Primitives as variables: raw palettes in Figma, bound straight onto layers.
- Semantic layer: role names with brand and theme modes, and components bound only to roles.
- Pipeline: tokens versioned and shipped to code, with reviewed changes and linting.
Most teams that say they "have tokens" are at stage three. The return on the work starts at stage four.
How AI helps, and where it doesn't
Agents read the system through Figma's MCP server. It pulls variables, components and layout data into the developer's IDE, and with Code Connect, generated code reuses your actual components instead of inventing new ones. An agent working from semantic tokens writes var(--color-action-primary). An agent working from a file full of raw values writes hex.
Audits at scale. Agents and scripts can sweep files for raw values, primitives bound directly to layers, and component tokens that skip the semantic tier. Figma's Check designs does part of this inside Figma, and Figma says it doesn't use LLMs or generative AI: suggestions are ranked by similarity to the current value, by variable naming and hierarchy, and by how often the team uses them.
Documentation drafts. AI can draft a $description for every token and usage notes for every component from how they are actually used. A person still has to review them, because a confident wrong description is worse than none.
Renames and migrations. Mapping old names to new ones across design files and code is tedious and well defined, which is exactly the kind of work agents do well.
The limit. AI follows the structure it's given. If primitives are visible everywhere, agents will bind them. If names are ambiguous, the generated code will be ambiguous too. AI speeds up a good token system and spreads the flaws of a weak one faster. For more on how AI is changing the work itself, see how AI is changing what it means to be a UX designer.
When to bring in senior help
Token architecture is cheap to get right early and expensive to fix once hundreds of screens and a codebase depend on it. ROSSUL has spent more than 20 years designing complex enterprise products, and we build design systems the same way we build products: from the workflows and constraints up, with the developers who will ship them. If you're planning a multi-brand system, untangling one that has drifted, or connecting Figma to your build pipeline, talk to us about your design system.
FAQ
What are the components of a design system?
Design tokens, UI components, patterns, documentation and governance. Tokens are the foundation, because every other part consumes them.
What's the difference between semantic and component tokens?
Semantic tokens name a role across the whole product, such as color/action/primary. Component tokens scope a role to one component, such as button/primary/background, and alias the semantic token. Create component tokens only where a component needs to diverge.
Should I use modes or extended collections for multiple brands?
Modes work on every paid plan, but plans cap the number of modes per collection. Extended collections are Enterprise only, and they let each brand inherit the core collection and override only what differs. If brand times theme exceeds your plan's mode limit, split the axes into separate collections or move to extended collections.
How do Figma variables map to the DTCG format?
Figma exports and imports variable modes as JSON in the DTCG format. Color (HSL and sRGB), dimension (px only), font family, duration and number are supported on import. String is accepted although it isn't an official DTCG type, and Figma-specific data rides in $extensions under com.figma.
Which Figma plan does a multi-brand token system need?
It depends on the number of brands, themes and integrations. Professional allows up to 10 modes per collection and Organization up to 20. Enterprise adds unlimited modes through extended collections, plus the REST API for variables.