Design Tokens as an Architectural Layer
Design tokens are design decisions expressed as data. They give design and engineering teams a shared, platform-agnostic source of truth that can be translated into CSS, SCSS, JavaScript, XML, or platform-native formats. The tokens concept was originally introduced by Salesforce to streamline design updates across multiple platforms, and the Design Tokens Community Group describes them as "a methodology for expressing design decisions in a platform-agnostic way so that they can be shared across different disciplines, tools, and technologies."
That description captures three distinct benefits. First, tokens establish a common language between designers, developers, product managers, and other roles, so everyone works from the same decisions. Second, tokens can plug into UI design tools, code generators, documentation systems, and token editors, letting changes flow directly into codebases. Third, tokens can be translated for the web, Android, iOS, and other targets without rewriting the underlying design decisions.
A Concrete Example: z-index
A useful illustration of tokens in practice comes from a large project around 2017 that used a Micro Frontend Architecture. Separate teams owned different parts of the UI, sometimes on the same page. When dialogs or toasts appeared over content areas owned by another team, developers had to coordinate z-index values. The effort often devolved into magic numbers—undocumented, arbitrary values that did not scale as the project grew and required expensive cross-team fixes.
The solution was to codify those values as design tokens, naming them for when they should be applied:
{
"z-index": {
"$type": "number",
"default": {
"$value": 1
},
"sticky": {
"$value": 100
},
"navigation": {
"$value": 200
},
"spinner": {
"$value": 300
},
"toast": {
"$value": 400
},
"modal": {
"$value": 500
}
}
}
That token file can be used by both designers and engineers. The same token source can also be translated into CSS variables or SCSS variables that each team consumes:
:root {
--z-index-default: 1;
--z-index-sticky: 100;
--z-index-navigation: 200;
--z-index-spinner: 300;
--z-index-toast: 400;
--z-index-modal: 500;
}
$z-index-default: 1; $z-index-sticky: 100; $z-index-navigation: 200; $z-index-spinner: 300; $z-index-toast: 400; $z-index-modal: 500;
Organizing Tokens in Layers
For tokens to scale across a large organization, the structure of the token files matters as much as the values they hold. Tokens should be organized in layers, moving from a set of available options to tokens that capture how those options are applied. Option tokens—for example, a full color palette—should generally stay private. Exposing every option to product teams increases the size of generated files and makes breaking changes more likely. Keeping options private and exposing a smaller set of applied tokens supports incremental, non-breaking updates and improves the developer experience.
Automated Distribution with a Translation Pipeline
To have updated tokens in the hands of product teams immediately after a designer commits a change, the translation and distribution process should be automated via a deployment pipeline. A translation tool—such as Style Dictionary, Theo, Diez, or Specify App—takes one or more token files as input and produces platform-specific code artifacts, plus optionally HTML documentation.
A prerequisite is storing tokens under version control. Plugins for design tools like Figma can integrate with Git providers, but it's important to note that the Git repository—not the design tool—should be treated as the single source of truth. This requires the plugin to synchronize bidirectionally between the repository and the design tool. Tokens Studio currently offers this kind of bidirectional syncing, with options for a target branch and support for both trunk-based and pull-request-based workflows.
With tokens under version control, the pipeline can build and distribute the dependencies product teams need:
- Check: Validate token files using a design token validator or a standard JSON validator.
- Build: Run a translation tool like Style Dictionary to generate platform-specific formats. This step can also build documentation.
- Test: Test either the token files directly (for example, checking color contrast) or the generated code. Storybook is a common choice here, with support for visual regression, accessibility, and interaction tests.
- Publish: Release packages to npm or other registries. Using Conventional Commits and a tool like semantic-release can fully automate versioning and multi-platform publication.
- Notify: Alert teams via email or chat so they can update their dependencies on their own schedule.
Distributing tokens as a versioned library gives product teams control over their upgrade cycle. A team can adopt a new style simply by updating a dependency, or the update may flow in indirectly through a component library that rebuilds on top of the token-based styles.
Adding a Manual Approval Gate
Automated quality gates are not always sufficient. When a human review before publication is required, a common fix is to deploy an updated version of the documentation to a preview environment. Using a tool like Storybook, this preview can show not only the tokens but also how they look integrated with the components. The approval itself can be handled by a pull-request workflow or by adding a manual approval step to the deployment pipeline itself.
At Thoughtworks, this kind of setup—versioned tokens, translation tooling, and a release pipeline—has let teams roll out smaller design changes across several front-ends in a single day.
Layering Tokens by Decision Type
Design tokens encode design decisions as data, but not every decision operates at the same level of detail. Treating tokens as a layered system—where general decisions guide more specific ones—keeps the architecture consistent and scalable. Trying to make an individual color choice for every new component quickly becomes impractical; it is far more efficient to define a palette and then decide where and how its colors get used. This approach reduces the total number of decisions needed while preserving a unified look and feel.
Three types of decisions, each building on the previous, cover the full design decision space:
- What design options are available to use?
- How are those styles applied across the user interface?
- Where exactly are those styles applied (in which components)?
These map to three categories popularized by Samantha Gordashko: option tokens, decision tokens, and component tokens.
Option Tokens: What Options Exist
These tokens—also called primitive, base, core, foundation, or reference tokens—define the styles available for use in an application: color palettes, spacing scales, font families, and so on. Not every option token needs to be used in the final UI, but together they represent the universe of reasonable choices. In the running color example, a palette might hold nine shades per color:
{
"color": {
"$type": "color",
"options": {
"blue-100": {"$value": "#e0f2ff"},
"blue-200": {"$value": "#cae8ff"},
"blue-300": {"$value": "#b5deff"},
"blue-400": {"$value": "#96cefd"},
"blue-500": {"$value": "#78bbfa"},
"blue-600": {"$value": "#59a7f6"},
"blue-700": {"$value": "#3892f3"},
"blue-800": {"$value": "#147af3"},
"blue-900": {"$value": "#0265dc"},
"grey-100": {"$value": "#f8f8f8"},
"grey-200": {"$value": "#e6e6e6"},
"grey-300": {"$value": "#d5d5d5"},
"grey-400": {"$value": "#b1b1b1"},
"grey-500": {"$value": "#909090"},
"grey-600": {"$value": "#6d6d6d"},
"grey-700": {"$value": "#464646"},
"grey-800": {"$value": "#222222"},
"grey-900": {"$value": "#000000"},
"white": {"$value": "#ffffff"}
}
}
}
Option tokens are highly useful for defining what exists, but they do not tell a developer how or where to apply those styles.
Decision Tokens: How Styles Get Applied
Decision tokens—sometimes called semantic or system tokens—specify the contextual application of option tokens. For the color example, that means rules like "grey-100 is for surfaces" or "blue-900 is the accent color." A decision token file might enumerate such mappings as aliases back to option tokens:
{
"color": {
"$type": "color",
"decisions": {
"surface": {
"$value": "{color.options.grey-100}",
"description": "Used as a surface color."
},
"background-disabled": {
"$value": "{color.options.grey-200}",
"description":"Used for the background of disabled elements."
},
"text-disabled": {
"$value": "{color.options.grey-400}",
"description": "Used for the text of disabled elements."
},
"text": {
"$value": "{color.options.grey-900}",
"description": "Used as default text color."
},
"accent": {
"$value": "{color.options.blue-900}",
"description": "Used as an accent color."
},
"text-on-accent": {
"$value": "{color.options.white}",
"description": "Used for text on accent color backgrounds."
}
}
}
}
For most developers consuming platform-specific artifacts, decision tokens are the useful layer: while an option-token color palette may hold hundreds of entries, only a small subset ends up being applied. Decisions are what matter when writing UI code.
This structure is often described as a “tiered” architecture; the notion of layers is more accurate, since no physical separation is implied. The two-layer pattern—options feeding decisions—looks like this:
Figure 5: 2-layer pattern
Component Tokens: Where Styles Land
Component tokens (or component-specific tokens) pin decision tokens to concrete UI pieces—not necessarily to code-level components. A button might be a full component in one app, while another app just uses a plain button HTML element; component tokens work for both, mapping text and background colors as needed.
These tokens can be grouped to reference several decision tokens at once. A button's tokens might cover primary/secondary variants, plus disabled states:
{
"button": {
"primary": {
"background": {
"$value": "{color.decisions.accent}"
},
"text": {
"$value": "{color.decisions.text-on-accent}"
}
},
"secondary": {
"background": {
"$value": "{color.decisions.surface}"
},
"text": {
"$value": "{color.decisions.text}"
}
},
"background-disabled": {
"$value": "{color.decisions.background-disabled}"
},
"text-disabled": {
"$value": "{color.decisions.text-disabled}"
}
}
}
To a degree, component tokens are simply decisions applied to specific items, but the mapping is rarely trivial for developers without design experience. Decision tokens give guidance; component tokens provide the explicit answer.
Figure 6: 3-layer pattern
Note: “Snowflake” cases do exist where a layer is skipped. A general decision might not exist (or not yet be made) for every component token, e.g., early in a project.
How Many Layers Does a Project Need?
The largest design systems commonly use two or three layers, but even a single layer cuts down everyday decisions substantially—for example, browsers implement up to 43 distinct CSS length units, making even a spacing-unit choice worthy of a reusable decision.
A three-layer setup promises the best developer experience, at the cost of more maintenance and more tokens—each new component adds tokens and grows the bundle. A two-layer start is a pragmatic default when the major design decisions already exist or are stable; the component layer can be added when the need becomes real. An extra component layer gives designers freedom to let decisions evolve later, which can itself justify the approach. In some cases, it can even be useful to seed layer one with component tokens and build the two more generic layers on top afterwards.
Ultimately, the layer count has to follow the project's flexibility and scalability demands.
Token Scope: Making Private and Public Boundaries
Option tokens are designers' companions; application developers—who consume the platform-specific artifacts—mostly care about the decision and component layers. Some design systems, like Salesforce Lightning, split tokens into private/internal and public/global groups. The distinction is worth establishing because it:
- guides developers toward the tokens intended for use
- shrinks options to improve developer experience
- reduces the emitted artifact size
- frees private tokens to change or disappear without triggering breaking changes
Hiding option tokens does have a flip side: developers depend on designers to surface every needed style as a decision or component token, which can stall work if designer availability is short or decisions are incomplete (project kickoffs are typical culprit). With no standard scoping mechanism in the design-token spec itself, your toolchain will likely require custom code.
File-Based Scoping in Style Dictionary
The quickest approach with Style Dictionary filters by file path—give different file extensions to option, decision, and component tokens, then filter out the layer you want hidden (e.g., option tokens):
const styleDictionary = new StyleDictionary({
"source": ["color.options.json", "color.decisions.json"],
"platforms": {
"css": {
"transformGroup": "css",
"files": [
{
"destination": "variables.css",
"filter": token => !token.filePath.endsWith('options.json'),
"format": "css/variables"
}
]
}
}
});
The output CSS then contains only the decision tokens, not the entire option palette:
:root {
--color-decisions-surface: #f8f8f8;
--color-decisions-background-disabled: #e6e6e6;
--color-decisions-text-disabled: #b1b1b1;
--color-decisions-text: #000000;
--color-decisions-accent: #0265dc;
--color-decisions-text-on-accent: #ffffff;
}
Flag-Based Scoping for Flexibility
Where file-based rules lack flexibility, add a scope flag to each token and filter on it:
const styleDictionary = new StyleDictionary({
"source": ["color.options.json", "color.decisions.json"],
"platforms": {
"css": {
"transformGroup": "css",
"files": [
{
"destination": "variables.css",
"filter": {
"public": true
},
"format": "css/variables"
}
]
}
}
});
{
"color": {
"$type": "color",
"decisions": {
"surface": {
"$value": "{color.options.grey-100}",
"description": "Used as a surface color.",
"public": true
},
"background-disabled": {
"$value": "{color.options.grey-200}",
"description":"Used for the background of disabled elements.",
"public": true
},
"text-disabled": {
"$value": "{color.options.grey-400}",
"description": "Used for the text of disabled elements.",
"public": true
},
"text": {
"$value": "{color.options.grey-900}",
"description": "Used as default text color.",
"public": true
},
"accent": {
"$value": "{color.options.blue-900}",
"description": "Used as an accent color.",
"public": true
},
"text-on-accent": {
"$value": "{color.options.white}",
"description": "Used for text on accent color backgrounds.",
"public": true
}
}
}
}
The generated CSS is the same as above—only the flagged decision tokens survive:
:root {
--color-decisions-surface: #f8f8f8;
--color-decisions-background-disabled: #e6e6e6;
--color-decisions-text-disabled: #b1b1b1;
--color-decisions-text: #000000;
--color-decisions-accent: #0265dc;
--color-decisions-text-on-accent: #ffffff;
}
If Figma variables serve as the design token source of truth, this flag matches the UI caption option and the plugin API’s hiddenFromPublishing property.
Weighting the Adoption Decision
Design tokens clearly support modern UI architecture, but they are not automatically the right tool for every project.
Pros include:
- Faster lead time on design changes
- A consistent design language across platforms and technology stacks
- Lightweight from an implementation viewpoint
Cons:
- Up-front investment to build automation
- Designers end up interacting with Git to some degree
- The underlying standard is still evolving
Good adoption scenarios:
- Projects spanning several platforms (web, iOS, Android) or maintaining several frontends
- High design-change frequency, where propagation must be controlled
- Big teams where design-to-development collaboration needs a formal backbone
- Teams already comfortable with CI/CD—the marginal automation cost is manageable
When to skip design tokens:
- Small projects with a contained scope and little design complexity—overhead wins over value
- Environments where design-change speed, consistency, and designer–developer collaboration are not pressing problems



