Unifying a
design system

Rebuilding the foundations of a live product from the primitives up, with one eye on converging with a new parent company.

92%
of colour usage now flows through the primitives
Lead designer
solo on foundations
Ongoing
Figma · Storybook · Confluence
Design system colour primitive boards covering neutral, brand, system and supportive palettes

Our app was built fast. Rushed to market in the way that leaves UX and development debt in its wake, and by the time I picked this up, that debt had a shape. Three separate design systems had grown up over time, each calling different tokens for colour, spacing, type and patterns, sitting under three genuinely different products plus a web platform.

The first was a discounts app that broadly mirrored the web. The second was a content, rewards and recognition app already mid-rebrand on a different foundation, brought in by a contractor and only partway applied when everything changed. The third was an acquired well-being app (goals, challenges, wearables) built on something different again. This wasn't one system drifting. It was three heterogeneous systems colliding, with no single source of truth, and new teams left to guess.

Then the ground moved. We were acquired by Edenred, a global meal-voucher provider with discounts layered on top, who wanted us to explore converging with their main app. From that point, every foundational decision carried a second requirement: fix our own house, and do it in a way that could eventually converge with theirs. We benchmarked against their SmartER system throughout. So this was never a tidy-up. It was rebuilding the foundations of a live product with one eye on someone else's.

Version one of the new-look UI shipped. Then a version two introduced a theming layer, and it was in the theming layer that the patterns broke.

In v1, the button family shared a deliberate logic: primary, secondary and tertiary related to one another in a way you could read at a glance. The theming layer recoloured the secondary button, and suddenly it sat too close to the chips and the selected tabs. The line between "this is a button" and "this is a chip" blurred, both for users and for us trying to say which pattern to use where. It was a lot of visual noise standing in for a system.

Two columns comparing the original and themed primary, secondary and disabled button styles

After the release, I sat down with the PM and the design principal to evaluate where we actually were and find a path forward. But any path forward ran straight through engineering. There was significant debt in how things had been built, and no proposal was going to land without the people who'd have to build it. So they were part of the thinking from the start: pulling the primitives and legacy sets out of the codebase, telling us what was actually feasible against the debt, and working through solutions with us rather than receiving them. Getting them bought into the direction wasn't a nicety. It was the thing that made any of it possible.

The decision was to slow down to speed up: go back to the primitives and rebuild the foundations properly, from the ground up.

I started by screenshotting every page of the app and bringing them together in Figma, mapped into sections. Not the Figma source, but the actual live app, as shipped, so we were auditing reality rather than intention. That snapshot alone earned its keep: it's the difference between what you think you built and what's actually in front of users.

Then I audited the primitives, starting with colour. And this is where the button turned out to be a symptom, not the cause. The colour audit surfaced the root. A single blue was doing three jobs: secondary buttons, selected tabs and selected chips all leaned on the same primitive and its relationships. The theming layer had broken that relationship, which is why the break showed up as a button but was never really about the button. Change one shade, and three patterns move.

The constraint shaped everything: this was a live product, so I couldn't force a clean-slate model without cascading breakage across components users were touching that day.

So the principle was safe expansion over contraction. Rather than collapse everything into a rigid model, I expanded the scale where existing relationships needed room, adding intermediate values like a 350 where a hard 300/400/500 would have snapped something. I moved naming to a colour-number convention: a default at 500, darker above, lighter below, so the relationships were legible and stable. And where a pattern clearly needed deeper work, such as colour semantics or contrast, I documented it as v2 rather than forcing churn now. Soft solutions first, clean solutions later, sequenced so development stayed unblocked.

Mapping table comparing current grey, cobalt and violet colour styles with the new primitive token scales and hex values

The foundations became a single source of truth again, rebuilt from the primitives up and benchmarked to converge with Edenred.

Since then, all the atomic components have been audited, redesigned, rebuilt, reviewed and implemented into the live app, which gives us a clear measure of colour adoption. 600 of 655 colour references in the codebase now resolve to a primitive token, around 92%. The remaining 8% is legacy, held deliberately and migrating incrementally rather than ripped out of a live product. Which is the whole philosophy in a single number: expand safely, migrate on purpose, and don't break what people are using today.