Home > Product Work > A design system sized to its product
A design system sized to its product
Only what the product uses, and the reasons for not building more · Design Technologist, solo · 2026
After co-leading a design system for eight brands at Dow Jones, I built one for a single product, StayKeel. The rules are the same. The question was how many of them one product actually needs.
The source is the theme
In a multi-brand system, one theme keeps every product on the same structure: same token names, different values. Here there is one product, but five kinds of content, and each needs its own colour on the card. So I treated the five sources as five themes. The colour is the theme, and the theme is where the token architecture starts.
Three levels. Raw values that no component ever reads. A scope per source that remaps six contextual tokens. And the contextual tokens, which are the only thing a component reads. The card does not know YouTube exists. Adding a source tomorrow is one block of CSS and zero lines in any component.
The Challenge
My Role
Architecture, tokens, components, documentation
- Designed the three-level token architecture and its naming.
- Built it in CSS custom properties, with a generated token page and a drift check.
- Decided which parts become components and which states exist.
- Mirrored the system in Figma from the product, not before it.
Scope
- 113 tokens, each with a documented role
- Five content sources as theme scopes, plus a fallback
- 38 components and 162 variants in Figma, every one shipped and labelled IN CODE; nothing designed that is not in production
- Figma variables that mirror the code one to one
Approach
Three levels, one rule, and the proof that it holds.
Three levels, and components read only the third
Level 1 is the palette: raw values, named by value, never read by a component. Level 2 is a scope per source, [data-source="youtube"], that remaps six contextual tokens. Level 3 is what the card reads: border-card, icon-card, bkg-pill and so on. A source lives in the scope, never in a token name. If it were in the name, the card would need a condition to pick it, and every new source would touch the component.
The proof, live
This is a demonstration card written for this page, with the same architecture and the same values. Change the source: six colours follow, the rating box doesn't, and the component code contains no colour name. In the product, a search for any source name inside the components, for colour purposes, returns nothing.
Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Resolved value (scope: YouTube)
| Contextual token | Resolved value (scope: YouTube) |
|---|---|
| --demo-color-border-card | |
| --demo-color-border-card-hover | |
| --demo-color-border-card-read | |
| --demo-color-icon-card | |
| --demo-color-bkg-pill | |
| --demo-color-ring-card-active |
A token is chosen for its role, never for its value
Two things that look the same today get two tokens if they do different jobs, because they will diverge. The rating label and the meta line are both small monospace capitals; they are two roles, so two tokens. The rule cuts both ways: a new role gets a new token even when its value already exists.
Component, pattern, guideline or one-off
One question decides: is it reused in more than one place? When the system was only the card, that test gave four components: source icon, tab selector, chip and rating row. Header, tab row, content area and footer are used once, so they stay inside the card. "Confirm before delete, inside the card" is a pattern. "Only source colours go through tokens" is a guideline. As the product grew to the header, the chat and the onboarding, the same test gave 38 components, each with its states, and every one of them is in production.
Figma came last, generated from the product
Alone, the product was the fastest place to decide. The Figma library was generated from the code afterwards, so the two cannot disagree: every variable mirrors a token, every component carries an IN CODE label, the component library follows Atomic Design (tokens, atoms, molecules, organisms, templates), and the token page in the app is read from the stylesheet itself, with a drift check that flags any token without a documented role.
Key Decisions & Trade-offs
What I chose, what I gave up, and what it bought.
| Decision Point | Options Considered | Why We Chose | What it bought |
|---|---|---|---|
| Where the source lives | 1. In the token name ( border-card-youtube)2. In a scope ( [data-source]) that remaps the same names | A name per source puts a condition in every component. A scope keeps the component ignorant and makes a new source a CSS block. | New source: 1 block, 0 component changes |
| Two levels or three | 1. Palette + semantic 2. Palette + scope + contextual | With two levels the theme axis has nowhere to go but the component. The middle level is small (six tokens per source) and is the whole theming mechanism. | Theme axis independent of components |
| Lightness over nominal level | 1. Every source at the same Tailwind step 2. Align on perceived lightness | Four sources sit at step 400. GitHub at 400 was ten L* points lighter than the cluster, so it is at 500. The palette aligns on how light the colour looks, not on what the scale calls it. | Sources read as one family |
| Colour is never the only signal | 1. Colour alone 2. Colour plus a text badge; read/unread on a different channel | Each card carries a short text label of its source, so it works in greyscale and with colour blindness. Read and unread never share the colour channel with the source. | Works without colour |
| What not to tokenise | 1. Full type scale, spacing and radius tokens 2. Only roles the product uses | A single product does not justify a scale. Type tokens are per role; spacing and radius are not tokenised yet. The day a second surface needs them is the day they get built, everywhere at once. | 113 tokens, none unused |
| How many components | 1. A component per visual element 2. Only what passes the reuse test | Parts used once stay where they are. Promoting them would add files, variants and maintenance for zero reuse. Nothing in the library is speculative: a component exists because the product ships it. | 38 components, 162 variants, none unused |
Where the source lives
border-card-youtube)[data-source]) that remaps the same namesTwo levels or three
Lightness over nominal level
Colour is never the only signal
What not to tokenise
How many components
What I Learned
The work that disappeared was the drawing. The work that stayed was deciding.
Generating Figma from the product removed hours of redrawing. It did not remove a single decision: which states exist, which parts earn a component, what stays out. Those were made before any tool ran.
A known outlier beats a hidden one
The article amber sits fourteen L* points above the other sources, and it is the most frequent colour in the feed, next to the brand peach. It is not a defect today. It is written down as the worst case for any dark theme, with the ticket that will pay for it. A palette that is honest about its weak point is easier to govern than one that looks perfect.
Alone, the product is the fastest place to decide. With a team, that changes.
Working solo, Figma was a canvas for early ideas and the code was the source of truth. Add one more designer and the Figma file stops being optional: it is where people see the system without opening the product. The right setup depends on who has to reach the decisions, not on the tool.
Figma can hold the truth. It cannot be the bottleneck.
If a decision lives somewhere only one role can open, that role becomes the bottleneck. The question is not which tool holds the truth; it is who can reach it.
See the tokens, or talk about them
The product is live. If you run a design system and are deciding how much of it one product really needs, I'd like to compare notes.