- Bemol Digital
- Senior Product Designer
- Design system · Foundations and components
- Mobile app and web
- In production · internal hub · component library in progress
The system came after the product
The product had been shipping for years, across an app and a mobile site, with its interface decisions already made. They just weren't written down, and they hadn't come from one place. A design system that starts from zero picks how things will be; this one had to find out how they already were.
So the first half of the work was archaeology rather than design: finding where a decision had already been taken, deciding whether it was worth keeping, and giving it a single home.
The size of the system
Scope figures, measured on 30 August 2026.
- 32
- components specified
- 116
- variants, across types and sizes
- 255
- documented states
Nobody had to decide what a button should look like. The hard part was finding the eleven places that had already decided, and picking one.
Three problems, none of them cosmetic
The same decision existed in several versions
Spacing, radii and text sizes had drifted apart across screens built by different people at different times. None of the versions was wrong on its own. Together they meant no screen could be assembled without re-deciding things that should have been settled.
Documentation that nobody opens is not documentation
A system only holds if a designer can answer a question faster by looking it up than by asking someone. That is a usability requirement on the documentation itself, and it sets the format: short answers, one place, findable under pressure.
Design and code had separate sources of truth
A component library that agrees with the design file only at the moment it is written will disagree by the next sprint. The system had to be defined so that both sides read from the same values instead of copying them.
Figma decides, everything else reads
Where the names just below this come from.
None of the places the system shows up decides anything. They all read, and Figma decides. Every colour, radius and spacing starts there and gets transcribed into a token file. There are 367 of them, in one file, in the open format the W3C maintains for design tokens.
One command turns that file into four things: the CSS custom properties for the web documentation, the Dart token class for the app, the constants the coded components consume, and a version for AI tooling. One command, four outputs, no hand copies.
The outputs differ on purpose. The code ones keep the whole chain: a component points at a semantic token, which points at a primitive. The agent's copy ships the resolved value, because whoever reads it wants the hex and has no use for the lineage.
The price is in the first sentence. The transcription is manual: someone opens Figma, sees what changed and writes it into the file. As long as that's true, the system is exactly as current as the last time a person looked.
Change the ramp step, or the radius, and watch what moves with it. None of these surfaces stores a colour or a radius: they all read the same variable, which is how one decision lands in four places at once.
The state comes from the matrix
The moment a component's states are drawn by hand, they start diverging: one screen has a focus ring, another forgot it, a third invented a different one.
Defining states as a matrix instead, every type against every state, turns a design question into a completeness check. If a cell is empty, that's a gap in the system, visible before it becomes a bug in production.
And the matrix settles things nobody would settle looking at one button. Two of them are in the grid below: loading borrows the pressed colours, because the button is still live while it waits; and the disabled secondary stays white, since what fades there is the border and the label.
| type | enabled | pressed | loading | disabled |
|---|---|---|---|---|
| primary | ||||
| secondary | ||||
| tertiary |
Both rules from the paragraph above are drawn here, and you can check them without reading: the loading and pressed cells carry the same colour, and the disabled secondary is the only one still white. Below, the three sizes: the small one is 32 pixels of visual inside a 44-pixel touch target.
Accessibility is a system property
Focus visibility, contrast and error handling are the first things to disappear when each screen is built separately, because each one is easy to skip and nobody notices until an audit.
Put in the system, they stop being a per-screen decision. Two of them are written into the spec, out of reach of whoever assembles the screen: an error is a border plus an icon plus text, never colour alone, because anyone who can't tell red from grey needs a second cue; and the height of the message is reserved before the message exists, or everything below it jumps the moment validation appears.
Contact use only.
Disabled: you can't reach it by use.
Type, clear it, leave out the @. The border swaps token and the message shows up in the 16 pixels already reserved below the field. The whole block occupies the same 72 pixels in every state, and nothing pushes anything.
The hub: four doors, one source
One address, and each role walks in through its own door.
A system scattered across different tools charges every person the same toll: working out where to look. The hub is the answer to that. It's an internal address that asks what you came to do rather than which tool you want to open, and the four doors are design, build for web, build for app, generate with AI.
Behind the doors, the same source. Every component has a specification written in Markdown, and that file produces the page a designer consults, the story a developer opens in Storybook, the widget running in the app's Widgetbook and the file an AI tool loads. Writing it in four places stopped being possible, because there is only one place to write.
A folder of documents describes the system from outside. Here the documentation is the way in: what's written in the Markdown is what shows up behind the four doors.
Someone arriving to design doesn't need to know that Storybook and Widgetbook are different things; they need to find the value of a colour. On the right, where the first door leads: the colour foundation, with the token name beside the swatch.
Web and app aren't two copies
The question that decides whether a cross-platform design system works is a mundane one: when the button changes, how many places have to be edited? If the answer is two, the system has already lost. The two will drift, and the drift surfaces months later, on a screen nobody is watching.
Here the specification is one, and each environment reads its own part of it. What changes between them is the language the component is written in, not the decision it carries.
On the left, the specification the team reads. On the right, the same button as a Flutter widget, with knobs for its properties. It's the code the app consumes, running here.
The fourth door is for the machine
If the team generates code with AI, the system has to live where the AI reads.
Designers and developers stopped being the only readers of a design system. When someone asks an agent for a new screen, the agent is the one choosing spacing and colour. Without the system nearby it invents a plausible value, which is the worst kind of error: it survives a quick review and matches nothing.
So the same source generates a package for tooling: an index of what exists and in what state, the specification for each component, the token values, and a single file for anyone who'd rather upload everything at once. A rules document goes with it, and the rule that matters most is the one that says to ask: if the token doesn't exist, don't approximate.
Each component appears with the state it's in and what it depends on. It's the same data behind the numbers at the top of this page, and it's what keeps an agent from assembling a component on top of another one that isn't ready yet.
What didn't ship
A component for every pattern in the product
Some patterns appear on one screen and will never repeat. Systematising them adds maintenance cost and buys nothing. A system that covers every case ends up too rigid to change.
The way back, from code to Figma
The outbound path exists: one token file becomes all four outputs with a single command. The return path doesn't. Changing a colour in Figma doesn't move the file on its own, and a person still transcribes the value. The drift between the two sides is recorded and reviewed rather than pretended away, but it remains the most expensive debt the system carries. And the next step isn't automating that transcription: it's not needing it. That isn't decided, and it isn't only my call, but it's the day I've been building for. With the values in a versioned file and the spec in plain text, changing who decides becomes a change of origin rather than a restart.
A focus ring that passes contrast
The system's ring is the brand blue at 32% opacity. Measured against white it lands around 1.9:1, and WCAG 2.2 asks for 3:1 on a focus indicator. The button matrix on this page shows the ring as it is. Fixing it only here would make the case look good and leave the product unchanged, and it's one of the first things I'd take into the next pass on the foundations.
Retrofitting every legacy screen at once
Rewriting a shipping product wholesale to satisfy the system is how systems get abandoned. New work uses it; old work migrates when it's touched for another reason.
Who did what
- Consolidating scattered decisions into a single set of foundations
- Structure and format of the documentation, so it answers questions faster than asking a person does
- Component states defined as a matrix rather than screen by screen
- The rules that come before components: grid, vertical rhythm, hierarchy
- The hub and the path out of it: one specification per component as the source, generating the documentation, the app showcase and the package AI tools read, built by me in pair with a coding agent
- Component library, built with the design team, still in progress
- Implementation and API of the coded components, with engineering
- Review of interface copy, with the writing side of the team
Where it stands
The 11 foundations and the screen-building rules are settled and in use, and since August 2026 they have an address: the hub serves all four doors from the same source. The component library is the part still under construction. Of the 32 specified, 9 are marked ready, and those are the ones running as code in both environments.
The hub is internal, and it will stay that way: it exists for the team building the product, not as a showcase. That's why this case shows screenshots instead of a link, and why the demos on this page were rebuilt from the real specification, which is as close as you can get to opening the tool.
Reflections
What's left when the files get old.
Every artefact here (tokens, matrices, rules) is a way of storing an agreement so it doesn't have to be reached again. The agreement is the hard part; the file is where it sits.
The measure I'd use has nothing to do with component count. It is how many questions stopped being asked, and how many screens now get built without anyone re-deciding what was already decided.