TL;DR
Vouchsafe’s UI ran on an 80+ color token palette across three different formats. Engineers and agents would both apply these tokens inconsistently.
I replaced them with 50 tokens in a single format, generated from a handful of core colours: you can read my technical write-up on Medium.
I then applied this colour token set across our 300+ components. I was also able to delete every outdated token from the codebase, leaving a clear set of tokens for both humans and agents to use.
Problem
In my first year at Vouchsafe, we iterated our frontend at least twice. Each time we did we took a different approach to tokens. The result was a mix of SCSS, TailwindCSS defaults, and custom theme tokens.
None of these systems had the rules of use clearly defined. What was the difference between grey-400 and grey-500? Why was our green-600 now closer to blue than green? If .vs-primary is deprecated, why is it still being used in new components?
Agent generated frontend changes made this harder to manage; without context it was guessing what it should use and often getting it wrong.
The solution here was not to create yet another palette, but a single system that would be clearer for both humans and agents to understand.
Approach
The core tokens stem from a single OKLCH value. I then used calc() and clamp() to calculate generic UI color tokens. Things like foreground, actions, backgrounds and surfaces.
I chose OKLCH over HSL because its lightnesses match how people actually see colours. In HSL, two colours with identical lightness can have very different contrast:
This made it easier to adjust the colours with formulas, instead of manually finding the right colours to contrast with each other. You can read the full technical write-up on Medium.
As well as the core colours, I also needed to add:
- Status colours. For example, errors are always red, so these tokens needed to be static.
- Third party UI tokens. Our video selfie capture ran on a third party provider with their own variables for custom colors, so I needed to create a set specifically for them.
- Brand elements. Vouchsafe uses gradients for different UI elements, like illustrations and badges.
- color-cta-primaryoklch(from primary clamp(0, l - 0.15, 0.55) c h)
- color-on-cta-primaryalways white
- color-fgoklch(from primary 0.25 c * 0.3 h)
- color-fg-mutedcolor-fg at 65% opacity
- color-cta-secondaryoklch(from primary 0.94 c * 0.4 h)
- color-on-cta-secondaryoklch(from primary 0.35 c h)
- color-errorfixed: oklch(0.5 0.2 27)
- color-error-bgfixed: oklch(0.95 0.03 25)
And this is an example description, in a smaller lighter text.
One other change I made was to introduce named tokens. This made it clearer what each token should be used for; the color-border token, for example, was now the default option for if you needed a border on something.
One trade off; older browser support
OKLCH is supported by >95% of browsers. To keep our colours working for older browser versions I created a static CSS file of equivalent hex codes. These are kept in sync manually. This way we maintain support while still taking advantage of a modern colour system in the app be default.
Impact
By the end of this project I had:
- Unified the tokens into one clear semantic system
- Deleted more than 4,000 lines of legacy CSS/SCSS files
- Applied the new tokens to more than 300 components across our design system
- Updated our Storybook with a tokens page to show every generated colour
Vouchsafe now has a single system with semantic tokens. This means that it’s no longer ambiguous which grey token to use for a border… now the only choice is color-border. Anything you want to be the primary brand colour? color-primary.
Whitelabelling
Our user-facing UI was used within a customer’s own journey. Right now the only option they have is to use our default purple branding.
With a dynamic palette, letting a customer add their own theme would be very simple; you would only need to change the core colour and the UI all rethemes in a way that doesn’t break it.
I only got to prototype this whilst at Vouchsafe, but the concept works like this:

One small trade-off; the formula for buttons currently caps how light they can get. If you chose a pale pink your buttons would end up a darker shade of pink. To fix this I would have updated the formulas to:
- Switch the text from white to black depending on the background
- Adjusted for middling colours (colours on which neither white not black would be readable)
Reflections
If I were to do this again, I would design for dark mode from the start. The tokens support it, but adding it now means retesting all of the components. Building this in at the beginning would have been much cheaper!