Design Tokens Are a Contract, Not a Color Palette
This post started as a note to my future self. Every few months I relearn the same lessons about design tokens, so this time I'm writing them down.
The setup was ordinary: Svelte at the edges, Playwright doing most of the heavy lifting, and a handful of scripts nobody wanted to own. It worked well until usage roughly doubled over a single quarter.
How we got here
Most of the complexity was accidental. Over the years we'd layered three different approaches to design tokens on top of each other, and each one made sense at the time.
When we finally measured, the numbers were worse than anyone guessed. Around 36% of the time spent on a typical change went into working around design tokens rather than building anything.
How it works
The fix turned out to be smaller than expected. Instead of adding another layer, we removed one and leaned on what Svelte already gives you out of the box.
Stripped of the project-specific bits, it looks like this:
export function debounce<T extends (...args: any[]) => void>(fn: T, wait = 200) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
It's tempting to generalize this into a library. Resist that urge until you've needed it in at least three places.
A few things that helped along the way:
- Make the default the safe choice, not just the documented one.
- Pair on the first few changes so the knowledge doesn't live in one head.
- Agree on what "done" means before the first pull request.
- Write the migration guide before writing the migration.
Caveats
None of this is free. The new approach is a bit more verbose, and newcomers occasionally ask why we don't just use Vite for everything. That's a fair question with a boring answer: we tried, and it made design tokens harder to reason about.
We still haven't solved hydration. That's a post for another day, and honestly, I'm not sure we'll get it right the first time either.
Takeaways
That's it for now. Next time I'll dig into form validation, which turned out to be closely related.