How I started using design tokens in a white-label product

I can now change a client's branding without editing every button and form. That was my goal for a white-label product: one application that different companies use under their own brands. Each company has its own colors, logo, and font. I wanted to maintain one interface, so a fix to a shared form would benefit every client using it.

At first, changing the appearance looked simple. If one company wanted a blue button and another wanted a green one, I just needed to change the color.

But the button also changes color when someone moves a pointer over it. The menu has a background color for the selected item. An input field has a border that appears when someone starts typing. I needed to account for those colors too.

I wanted to configure a brand in one place without searching through every screen. That is how I started working with design tokens.

First, I put the values in one place

I started giving names to colors, fonts, and corner rounding. Reusable parts of the interface, called components, read these named values instead of storing their own.

A design token is a named value for a design choice. For example, I define the color of the main action button once. Every instance of that button uses the same token.

Calling a color "blue" seems reasonable at first, but the name does not explain where to use it. After a brand change, "blue" can even contain a green value. The code still works, but the next developer has to figure out why.

I started naming values by their purpose. For the main action button, I use --primary for the background and --primary-foreground for the text.

In CSS, the language for styling web pages, these tokens become variables that components read by name. The button uses them like this:

css
.button-primary {
  background: var(--primary);
  color: var(--primary-foreground);
}

The button reads its background and text colors from those variables. The same component works for both brands:

SettingFirst brandSecond brand
Main colorBlueGreen
Button textWhiteWhite
Button codeThe sameThe same

When I fix the button's behavior, both brands get the fix. I do not have to copy changes between two versions of the component.

Why I gave each brand its own file

I use a brand.css file for each client's appearance settings. It contains the brand colors, font, and other supported settings.

Here is a shortened example for a green brand:

css
/* brand.css */
:root {
  --brand-primary: #008055;
  --brand-primary-foreground: #ffffff;
}

The first value sets the main brand color, also called the accent color. The second sets white text on that color. A blue brand uses a different value in its own brand.css, such as #004fff.

The token package maps these brand settings to the variables that the button reads. Here is a simplified view:

text
Client's brand.css
--brand-primary: green
           ↓
shared token --primary
           ↓
button background turns green

I now have a separate place for each kind of change. When a client's appearance changes, I edit their brand.css. When the button itself needs work, I edit the shared component.

This helps me understand client requests too. A different color or font fits within the brand settings. Moving fields or adding a registration step requires a change to the form itself.

Where I store tokens and how I generate CSS

I keep the shared values in src/tokens.tokens.json. It uses JSON, a text format for storing structured data. The file describes colors, fonts, rounded corners, and the references between tokens.

The application does not read this JSON file directly. A build script prepares the values for use in the application. My script, scripts/build.mjs, reads the JSON file and creates dist/index.css. This stylesheet contains CSS variables, including --primary, --primary-foreground, and --border.

text
src/tokens.tokens.json
Shared values and references between tokens
                │
                │ scripts/build.mjs reads this file
                ▼
dist/index.css
CSS variables for the application

I load the generated CSS in the application with this import:

css
@import '@global-torque/design-tokens/css';

The import points to dist/index.css. The application loads the client's brand.css after the shared styles, so the brand settings take effect.

For shared values, I edit src/tokens.tokens.json. For a client's brand settings, I edit their brand.css.

I do not edit dist/index.css by hand. The next run of the build script would overwrite those edits.

One main color was not enough

After I set up the main colors, I still had their lighter and darker shades to handle.

For example, a button turns green while the selected menu item keeps its pale blue background. This happens when someone picks the background color by hand and stores it as a separate value. The colors look related, but the code has no link between them.

I started calculating some shades from the main color. For a pale accent background, I mix the brand color with a light background. The browser does this through the CSS function color-mix().

In this example, the mix contains 6% of the main color. With a white background, the result looks roughly like this:

text
Blue accent  + white background → pale blue background
Green accent + white background → pale green background

When the accent color changes, the browser recalculates the pale background too. I no longer need to pick a separate value for it.

I still check the resulting colors together. A formula can produce a shade that looks wrong next to the other colors, especially when the new brand differs from the previous one.

Why I kept status colors separate

A brand's green and the green for a successful action can look identical. That makes it tempting to use one value for both.

If I link the success color to the brand, a client with a red accent gets red success messages. The text says everything went well, while the color looks like a warning.

I kept success and error colors separate from the brand colors:

ElementWhat sets its color
Main action buttonThe client's brand
Success messageShared rules for successful actions
Error messageShared rules for errors

I became more careful about values that happen to match. Two elements can share a color today and need different colors later.

I also check text and background colors as a pair. White text is easy to read on a dark button, but it can disappear on a light one. The text color has its own setting for that reason.

What I gained

I can now plan work for a new brand separately from changes to the product. I started by wanting faster color changes. The work needed to set up each new client and support them afterward became more important to the business. I wanted to reuse the product I already had without collecting separate client versions.

At first, I made the package more complicated than it needed to be. It had seven ways to access tokens, including JavaScript, JSON, and CSS. The package generated different files for those formats, and I had to maintain and check them. The values themselves were hard to follow too. A button's background went through a button-specific token, a general action color, and finally a base color. I had to follow that chain to find the value to change.

The code worked, but even a small request meant tracing those references. That made the work harder to estimate. A request to change the appearance could mean one edit or a search through several files. Maintaining the styling system took attention away from work on the product.

Now the package provides one CSS file, and each client's settings live in their brand.css. If a client wants a different font or color, or more rounded buttons, I know where to change it. I can estimate that work separately from a new feature or a change to a form's behavior.

Work on shared components benefits every client who uses them. I can improve a form or fix a button once and use that improvement across brands. The next client starts with the improved interface.

I can also change brand settings on an open page without rebuilding the application. This gives me a way to build a brand preview tool. With that tool, clients and I could try their colors and font, then choose a version together before launch. Each variation would use the same application.

Setting up the next brand is easier to plan because I can reuse work I already finished. That leaves more time for features clients need, with less time spent figuring out how to recolor the same button.

Migration-service developers usage instruction

article Migration-service developers usage instruction image

One Platform, Many Configurations: Global Torque’s Modular Architecture

article One Platform, Many Configurations: Global Torque’s Modular Architecture image

May Development Update

article May Development Update image