All Articles

How to Build and Scale Design Systems: Setting Up Design Tokens in JavaScript and Figma

Image from Unsplash by Margarida Afonso
Image from Unsplash by Margarida Afonso

In my last post, I rambled about choosing between Web Components and front-end frameworks when building your design system. I realize I might have jumped the gun a little by not discussing the foundational layer that underpins all design systems: Tokens.

Most front-end developers will understand what atomic UI elements are (e.g. Buttons, Modals, Text items, etc). The concept of tokens, however, can be a bit of a foreign concept until you’ve had to scale and maintain UIs in a complex setting.

What Are Design Tokens?

From a developer’s perspective, the best way to think about design tokens is to think of them as a set of “code constants” or “config variables”.

When building out a reusable <Button> component, you could easily define its background color as:

.my-button { 
    background-color: #006E6E; 
}

This is fine for small projects that only serve a limited number of use cases. At scale though, you start running into problems. For example, if your designer wants to launch a rebrand effort to update #006E6E for all interactive components across the board, they can’t simply run a “find-and-replace” operation, because:

  • Visual consistency is difficult to enforce in large organizations: If your organization is large and has multiple teams owning multiple frontend runtimes (e.g. various public-facing customer apps, merchant apps, and internal apps), how do you know which instances of #006E6E need to be replaced, and which don’t? How do you know which frontends need to be aligned with your design system, and which don’t?

  • Visual consistency can also be difficult to enforce in smaller organizations: Even if your organization is on the smaller side and has most of its frontend code packaged as one or two applications (e.g. just one public-facing app and one internal app), #006E6E may be used across different contexts (e.g. in both interactive and non-interactive components). Again, how do you know which instances of #006E6E need to be replaced, and which don’t? Can you safely trust an LLM to analyze every use case and make that distinction for you — without you having to do a manual audit, given that your visual regression tests will likely fail?

The solution here is to define each color as a token with an appropriate name tracking semantic meaning:

:root {
  --ds-color-interactive-background: #006E6E;
}

.my-button {
    background-color: var(--ds-color-interactive-background);
}

With a CSS variable defined for this token, you can easily do a global lookup of --ds-color-interactive-background (where ds stands for “design system”), and you’ll know exactly how this specific color token for this specific use case (i.e. coloring interactive elements) is actually being used.

In practice, your tokens are going to look a bit more verbose than just --ds-color-interactive-background. You’re also going to need more than just CSS variables. To define a truly robust token system, you’ll need multiple layers of nesting that cover the full spectrum of use cases.

Defining Base and Semantic Tokens in JavaScript

Think of your design system as a large JavaScript object - where your keys are just design labels and your values are actual CSS values. We’ll use dot notation to cover what the schema of this object should look like.

At the top-level, you’ll want to define a set of base and semantic tokens.

Base Tokens

Your base tokens are essentially raw pointers to style values (e.g. #006E6E, 1rem) while your semantic tokens reference your base ones.

Your base tokens will cover things like typography, sizing, and color:

// Base color token
ds.base.color.teal[40] = `#006E6E`;

// Base size token
ds.base.size[10] = `10px`; // or size.base = `1rem`

// Base typography token
ds.base.font.family.default = `Roboto`;

// ...

Semantic Tokens

Your semantic tokens, as their name suggests, provide semantic meaning and map to these base tokens:

import { color, size, font } from "src/base";

// Semantic color tokens for interactive text
ds.semantic.color.light.ink.interactive.hover = color.teal[30];
ds.semantic.color.light.ink.interactive.pressed = color.teal[20];

// Semantic size tokens
ds.semantic.size.small = size[20];
ds.semantic.size.medium = size[24];

// Semantic spacing tokens
ds.semantic.spacing.small = size[8];
ds.semantic.spacing.medium = size[12];

// Semantic typography tokens for desktop text
ds.semantic.typography.desktop.heading.family = font.family.default;
ds.semantic.typography.desktop.heading.medium = size[36]; 
ds.semantic.typography.desktop.heading.lineHeight = size[40]; 

// ...

Color Themes and Device Types

As with any good API contract, you’ll want to ensure your token schema is open-ended enough that you can support a variety of use cases. Specifically, you’ll want some concept of:

  • Themes (e.g. Light vs Dark color tokens), and
  • Device Types (e.g. Mobile vs Desktop size tokens)

Just make sure your token schema covers this by having dedicated keys for each variant you need:

  • For colors, define your first theme as semantic.color.light (or semantic.color.default) - that way you can always add more color themes in the future.
  • For device types, start by at least using semantic.typography.desktop and semantic.typography.mobile. You can make this even more granular if you’d like (e.g. semantic.typography.sm and semantic.typography.md) if you want to define multiple breakpoints.

What Does This Mean for UX Designers?

If you’ve never worked with Figma, you may be surprised to know that the tools that UX designers use in Figma map pretty nicely to the tools that front-end developers use in code. Especially with Dev Mode and Figma MCP, the gap between design and front-end code is becoming much leaner.

Figma absolutely has a concept of variables. Designers can toggle variable values by using modes.

My recommendation is to include Figma modes in your token schemas. If a designer is able to toggle between Light and Dark modes on Figma, that toggle should be represented as a key in your color schema as color.light and color.dark.

Translating Figma Variables to Code

At this point, you might be wondering: “Do I really have to define these tokens in code myself? Is there a way to import them from Figma?”

Tokens Studio is the leading solution provider in this area. It comprises a collection of tools for managing design tokens and supports a Figma plugin.

In Figma, you work with variable tables, while in Tokens Studio, you work with token sets. Conceptually, they aren’t too different — these are still just “labeled styles” that can be repeatedly applied to different selections of UI.

Tokens Sets in the Tokens Studio Figma Plugin
Tokens Sets in the Tokens Studio Figma Plugin

That said, Tokens Studio provides a somewhat more intuitive CX for managing tokens. You can set up JSON exports that automatically sync with version control systems like GitHub, GitLab, etc. Code (e.g. JSON) is primarily treated as the source of truth:

  • If a designer wants to make a token update, they’d update the token set in Tokens Studio and “push” the change, which automatically starts a code review.
  • If a developer wants to make a token update, they’d submit a code review themselves.

After the code change is merged, a designer can “pull” the change into the Tokens Studio plugin and sync the Token Set changes with Figma variables.

The trade-off here is that while Tokens Studio has a free tier, the key features that designers and developers will care about the most, such as theme management, only come with the paid plans. I would also be surprised if Figma doesn’t eventually establish parity with Tokens Studio by building all of this out natively.

Figma has a rich plugin system and there are many other plugins that aim to bridge the gap between Figma variables and code, but the Tokens Studio model is probably the most established one. If your organization uses a supported version control platform like GitHub and you don’t mind the cost, I’d recommend using Tokens Studio.

At Amazon, we have our own internal version control system, which makes this a bit trickier to set up. However, there is an open-source build tool that originated from within AWS that you’d be pleased to know about…

Style Dictionary

Another question you might have is: “If all my tokens are in JavaScript (or JSON), how do I convert them into a format compatible with all the consumers I need to work with, like CSS, SCSS, iOS, and Android?”

Style Dictionary is a build tool that helps you convert your tokens into the formats you need them to be in. Using Style Dictionary means you can do:

@import 'my-design-system/dist/tokens/css/tokens.css';

.my-button {
  background-color: var(--ds-semantic-color-light-background-interactive-default);
}

or

@use 'my-design-system/dist/tokens/css/tokens.scss' as ds;

.my-button {
  background-color: ds.$ds-semantic-color-light-background-interactive-default;
}

or

import { ds } from "my-design-system";

const myButtonStyle = {
  backgroundColor: ds.semantic.color.light.background.interactive.default,
};

Check out the examples in the docs for how you can set this up.

Tips

Here are some personal pointers / callouts I have for using Style Dictionary (SD) and for your token schemas in general:

TypeScript-First Approach

I strongly recommend setting up your pre-transformed tokens with TypeScript. This gives you to ability to vend not just raw tokens, but their types as well. Just make sure to point Style Dictionary to the JS entrypoint file emitted by the TS compiler (e.g. /dist/js/index.js).

This is especially useful in situations where you may want to bind attributes or props on your atomic UI components to the types of your semantic tokens (e.g a padding prop on a <Box> component can be bound to keyof typeof ds.semantic.spacing).

Some of your token types may seem duplicative (e.g. defining TS types for base size tokens and then re-defining their actual values in a separate file), but that’s a minor cost in my opinion. You can try using const assertions to get around this (e.g. const SIZES = [0, 1, 2, ...] as const) but you’ll likely still need quite a bit of coercion (e.g. as Partial<SizeTokens>) since the generated types are narrow.

Token Values as Objects

A quick one: If you’re running into trouble with the SD build process, it may just be that you’ve not defined your token value in the format of { value: string } / { value: "10px" } — so watch out for that!

Be Precise on Size Abbreviations

Your tokens are seldom going to just span small, medium, large. You’ll often find yourself dealing with values like xxs and xxl. With your designer, make sure that you establish early if you should be using 2xs or xxs, 3xl or xxxl.

Not All Tokens Map Cleanly to CSS

Tokens like padding are easy to translate to code since the same padding exists as a property in CSS, but some other context-specific tokens like paragraphSpacing do not have a clear mapping to CSS. You’ll need to work with your designer to understand how this should be implemented at a component-level.

paragraphSpacing, for instance, may just be a bottom margin on a multi-line <Text> element.

All That Said… Moderate the Usage of Your Design Tokens Library

With all that effort that we’ve put into establishing design tokens, it seems desirable to share your tokens library as widely as possible and onboard as many front-end consumers as you can.

However, in reality, you’ll be better served by sharing reusable UI elements to these consumers instead. Internally, these reusable UI components would use design tokens:

A design system pyramid featuring tokens, atomic elements, composite components, and patterns
A design system pyramid featuring tokens, atomic elements, composite components, and patterns

Your front-end consumers should not need to worry about whether they should be using a token like background.interactive.hover for their interactive elements, or a token like spacing.large to pad their top-level page templates. This should come out of the box with the reusable <Button> or <Page> components that you’ve built for them. The only time a consumer should be using a token directly is if they have exceptional use cases that require them to build bespoke components.

Conclusion: Tokens Behind Components

In the long run, a design system should simplify, not complicate. This means creating reusable abstractions and reducing the amount of overhead developers have to deal with when constructing UIs.

Design Tokens are the most important layer, but typically receive the least attention, because they need to be abstracted behind reusable UI components in order for front-end developers to yield development benefits. I’ve seen situations where engineering teams build components without working with designers to align on tokens, and the divergent sources of truth result in greater long-term churn.

At this point, it seems fitting for us to move up the visual hierarchy and examine how to build reusable UI components. I’ve already covered framework choice in my last entry, but there are so many other factors that deserve discussion.