When our team, consisting of myself as a frontend developer and Roman Vasilev as a designer, began working on a component library, it became clear that we needed to create an in-house design system based on design tokens. We collaborated to build a tool that met these needs. Throughout the process, we gained valuable insights that we're now eager to share. We hope our experience can help others tackling similar challenges in their projects.
Introduction
At the time of implementation, Figma only had styles, and Figma tokens were not yet available. Since we have a scalable, multi-solution web application distributed around the world, we looked for tools and processes that would help us maintain a single source of truth and synchronize every change instantly.
After research, we settled on the following flow that is most convenient for our purposes:
- Figma as source of truth, where all things started.
- Tokens Studio as a Figma plugin that give ability to store, manage, synchronize and update tokens with repositories.
- Style Dictionary as tool that allows transform design tokens in any format.
- Tailwind as our CSS framework.
Design Tokens
Before you start using tokens, you obviously need base styles. We took Tailwind as the core for almost everything except colors. While Tailwind has great palettes, they were too vivid for our brand, especially when applied to charts, and they were difficult to handle with a dark theme. So, we looked at Radix UI palettes, customized them slightly to fit our tokens approach, and created versions for dark mode as well.
Once the base is ready, we need to agree on a naming structure for tokens that is easy to understand for both designers and developers. Naming design tokens might seem like a small detail, but it’s super important.
Looking at popular Design Systems, we want design token names to explain their purpose and usage, with each part providing specific information while remaining as understandable and short as possible. We arrived at the following naming pattern:
- Category: Indicates the type of visual design attribute, such as color, spacing, or elevation.
- Property: Specifies the UI element the token applies to, like a border, background, or shadow.
- Context: Provides additional context about the token’s role, such as color usage, emphasis, or interaction state.
We combined our semantic tokens into the following groups, which indicate property: Primary, Secondary, Typography, Icons, Backgrounds, Surfaces, Borders, Actions, Severity (Success, Error, Warning, Trivial), Translucents, and Accents.
Each group then has a context that indicates emphasis or interaction, such as: Default, Subtle, Tint, Hover, Active, and Focused.
On top of that, we added themes that include light mode, dark mode, and high-contrast mode. Themes can also include non-color elements, such as compact, normal, or wide views, or custom typography styles. This is convenient for customization when a customer buys a white-labeled product.
Tokens Synchronization
When you agree on a naming structure and create all styles/variables in Figma, it's time to start syncing them with your codebase repositories. As tool for synchronization with choose Tokens Studio plugin. This plugin ensures smooth, bidirectional synchronization between design files and code repositories, turning design tokens into a unified source of truth. This helps bridge the gap and enhance collaboration between design and development teams. Tokens Studio has a supportive community on Slack that is ready to help with any questions, along with well-written documentation.
This is where the magic happens. When you need to make changes, just a few clicks, and the updated styles are pulled into the repository, instantly updating everything.
Now, let's look at this from a developer's perspective!
Setting up a Next.js project
As a starting point for building a test project, we used a Next.js Typescript template. This boilerplate implements core principles of JAMstack and allows to quickly create serverless applications.
To set up the project environment, it is necessary to install the following dependencies:
// package.json
dependencies: {
"style-dictionary": "",
"token-transformer": "",
"tailwindcss-box-shadow": "", // opt
"tinycolor2": "", // opt
}
Additionally, don't forget to regularly update your dependencies to ensure that token modifications are processed correctly.
The project has a standard file structure. The design tokens are placed within the styles directory. Additionally, the build.js file, which runs Style Dictionary, is located in the scripts folder.
├── styles
│ ├── scripts
│ │ ├── build.js
│ │ └── fns.js
│ ├── tokens
│ │ ├── input
│ │ │ ├── base
│ │ │ │ └── global.json
│ │ │ └── themes
│ │ │ ├── dark
│ │ │ │ └── dark.json
│ │ │ └── light
│ │ │ └── light.json
│ │ └── output
│ │ ├── dark.json
│ │ ├── global.json
│ │ └── light.json
│ ├── dark.css
│ ├── global.css
│ ├── index.css
│ └── light.css
Automating Design Token updates
The Figma project is synchronized with the repository, and when the designer pushes any changes, that triggers a pipeline, which transforms raw data into CSS variables.
Design Token files must go through several transformation steps to return valid style sheets. The token-transformer utility replaces the references with calculated values so that the JSON object conforms to the Style Dictionary standards. The --expandTypography option can be used to convert every font-related property into an individual object.
// package.json
"scripts": {
"transform-tokens-dark": "npx token-transformer app/ui/styles/tokens/input/themes/dark app/ui/styles/tokens/output/dark.json",
"transform-tokens-global": "npx token-transformer app/ui/styles/tokens/input/base app/ui/styles/tokens/output/global.json --expandTypography",
"transform-tokens-light": "npx token-transformer app/ui/styles/tokens/input/themes/light app/ui/styles/tokens/output/light.json",
"transform-tokens": "yarn transform-tokens-light && yarn transform-tokens-dark && yarn transform-tokens-global",
"tokens": "node app/ui/styles/scripts/build.js"
}
Commands in package.json support design token workflow:
// Transform design tokens
yarn transform-tokens
// Build and update CSS variables
yarn tokens
Global style settings
In global.json file, we've established a foundational set of design variables that are essential across the entire project. This includes crucial aspects like typography, size and z-index.
Style Dictionary allows us to define functions and then to modify input values. For example, you can specify the letter-spacing property in ems.
// build.js
function transformLetterSpacing(value) {
if (value.endsWith('%')) {
const percentValue = value.slice(0, -1);
return `${percentValue / 100}em`;
}
return value;
}
StyleDictionaryPackage.registerTransform({
name: 'size/letterspacing',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'letterSpacing',
transformer: (token) =>
transformLetterSpacing(token.value)
});
Theme-specific styles. Dark and Light mode definitions
In dark.json and light.json files, we focus on theme-specific styles, primarily defining colors and shadows tailored for dark and light modes.
The transform function can combine the number of parameters that define the appearance of a box-shadow into a CSS variable. The resulting value can be used in the theme configuration file after installing the tailwindcss-box-shadow plugin.
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {},
plugins: [require('tailwindcss-box-shadow')]
}
// build.js
StyleDictionaryPackage.registerTransform({
name: "shadow/css",
type: "value",
transitive: true,
matcher: (token) => token.type === "boxShadow",
transformer: (token) => {
const shadow = Array.isArray(token.value) ? token.value : [token.value];
const value = shadow.map((s) => {
const { x, y, blur, spread, color } = s;
return `${x}px ${y}px ${blur}px ${spread}px ${tinycolor(color).toHslString()}`;
});
return value.join(", ");
},
});
Color Modifiers
Tokens Studio uses modifiers for fine-tuning color tokens, including lightening, darkening, mixing, and adjusting opacity.
By creating transformers like color/hslAdjust in Style Dictionary, we can adapt tokens, darkening colors by a specified percentage and returning the result in HSL format. This approach allows for dynamic visual changes in interface elements, for example, darkening the hover token by 27% when the user hovers over it.
// light.json
"hover": {
"value": "{color.blue.500}",
"type": "color",
"$extensions": {
"studio.tokens": {
"modify": {
"type": "darken",
"value": "0.27",
"space": "hsl"
}
}
}
}
// build.js
function resolveTokenValue(token, dictionary) {
if (
typeof token.value === 'string' &&
token.value.startsWith('{') &&
token.value.endsWith('}')
) {
const resolvedToken = dictionary.getReferences(token.value);
return resolvedToken ? resolvedToken.value : token.value;
}
return token.value;
}
function transformHSL(token, dictionary) {
const resolvedValue = resolveTokenValue(token, dictionary);
let color = tinycolor(resolvedValue);
const modification = token.$extensions?.['studio.tokens']?.modify;
if (modification && modification.type === 'darken') {
color = color.darken(parseFloat(modification.value) * 100);
}
return color.toHslString();
}
StyleDictionaryPackage.registerTransform({
name: 'color/hslDarken',
type: 'value',
matcher: (token) => token.type === 'color' && token.$extensions,
transformer: (token, dictionary) => transformHSL(token, dictionary),
});
Theme
Finally, during the building process, a set of CSS variables will be created. Style Dictionary will add selectors to style sheets as a combination of a :root pseudo-class and a themed class. These can be used later to swap from light to dark mode.
// build.js
files: [
{
destination: `${theme}.css`,
format: 'css/variables',
selector: `:root.${theme}`
}
]
The Tailwind configuration file stores the object that represents the current theme. And Design Tokens can be attached to component styles through CSS variables. Thus, we can update the external presentation of the User Interface by changing the values of the tailwind.config.js.
// tailwind.config.js
theme: {
colors: {
primary: {
DEFAULT: 'var(--color-primary-default)'
}
}
}
To maintain consistency in UI design, we created a ThemeProvider component and placed it at the top-level of the React tree. That wrapper uses the Context API to pass the current theme data down to child components.
// App.tsx
export const App = () => (
<ThemeContextWrapper>
{/* children */}
</ThemeContextWrapper>
);
Conclusion
Creating a Design System based on design tokens using Figma, Tokens Studio, Style Dictionary, and Tailwind has revolutionized our approach to developing component libraries and screen layouts. This method ensures instant synchronization between design and development, significantly enhancing workflow efficiency and reducing implementation time. By maintaining design consistency across all products, our token-based system serves as a robust foundation for both current and future projects. We believe our experience in implementing design tokens can be a game-changer for your next project, enabling you to build a more organized, maintainable, and efficient Design System that seamlessly bridges the gap between design and development.
Top comments (0)