Case study
A universal, framework-agnostic design system across four teams
Four product teams each built their own components, so the brand looked different everywhere. A token-first design system, adopted incrementally through workshops and pairing, reached 90% adoption and cut brand-change rollout from weeks to hours.
- Component variations
- 1000+
- Team adoption after 12 months
- 90%
- Brand-change rollout
- weeks to hours
- Component libraries
- 4 to 1
- Runtimes unified
- 4
Context
I was one of two senior engineers on a dedicated design-system team, alongside one UX/UI designer. Four product teams, three on web and one on mobile, had each been building their own components from scratch.
There was no shared library to diverge from; there was nothing shared at all. Every team's buttons, inputs and layout primitives were built independently by whoever picked up that ticket, each styled from their own reading of the brand guideline. The result was visible design inconsistency between products that were supposed to look like one company. There was no way to support more than a single brand either, since colour, spacing and type weren't defined anywhere central enough to vary.
The constraint
The three web teams split across #react and #vuejs, and the mobile team shipped separate native apps for Android and iPhone. Any fix had to work identically across four runtimes with nothing in common at the code level.
It also had to be multi-brand from the start, not bolted on later, because the whole point was to stop hard-coding one brand's values into every component. And it couldn't wait for a finished product to prove itself: a system that only shipped value after a year of silent building would have lost the room long before launch, in an organisation that had already watched one cross-team initiative stall out. It had to earn adoption incrementally, component by component, while it was still being built.
What I did
- Design tokens (Style Dictionary) to Team's own components
- Design tokens (Style Dictionary) to Design system, grown incrementally
- Team's own components to React
- Team's own components to Vue.js
- Design system, grown incrementally to React
- Design system, grown incrementally to Vue.js
- Design system, grown incrementally to Android
- Design system, grown incrementally to iPhone
I treated this as two separate products with two separate adoption curves, not one big release.
- 1
Ship tokens first
Used Style Dictionary with separate compiled exports for web and mobile: a team could pull in brand-correct colour, spacing and type the same week, without waiting on a single component.
- 2
Grow the library incrementally
Built the component library on top of that token package a handful at a time, not as a finished 1000-variation system dropped on day one, releasing each one as soon as a real team needed it for a real screen.
- 3
Make adoption a relationship, not a release note
Regular team syncs, company-wide demos, and hands-on workshops. Live pairing, sitting with an engineer and migrating their actual component in front of them, moved the needle most.
- 4
Rotate engineers through the design system team
Each team seconded an engineer for a sprint or two. They shipped a real component back to their own team and left as a built-in champion for integration questions.
- 5
Make adoption measurable, not debatable
Wrapped every component in a #mixpanel tracker, and audited every repo's
package.jsonfor the tokens and design-system packages rather than relying on self-reported status. - 6
Hold the team to two explicit SLOs
Visible to the teams it served, not just tracked internally: 90% adoption within 12 months, and under two sprints at the 95th percentile for API-flexibility requests raised on a single Jira board instead of scattered Slack threads.
- 7
Back every release with a real test net
Unit tests, React Testing Library, end-to-end tests, and #chromatic visual regression across Chrome, Firefox and Safari, gated in CI, with a live #storybook instance for docs and diffs.
- 8
Automate the mechanical parts of migration
Wrote jscodeshift codemods for prop renames, import paths, and class name swaps, so pairing time went to the genuinely tricky cases.
- 9
Put a sunset date on the old components
Agreed with each team's lead up front, once the replacement had shipped, so migration had an actual deadline.
- 10
Keep Figma and code in sync
Synced #figma token values into the Style Dictionary source via a CI check, so a change made in Figma failed the build until it was reflected in the token package.
Outcome
Both SLOs held. 90% of teams had migrated onto the shared system
within twelve months, measured directly from the package.json audit
rather than self-reported status. Request cycle time stayed under the
two-sprint target at the 95th percentile once the Jira board gave
requests a single queue instead of scattered Slack threads.
The adoption curve tracked the human investment almost exactly: it moved fastest in the months with the most workshops and pairing sessions, not the months with the most components shipped. A brand change that used to mean four teams each hand-editing their own components now means editing the tokens package once and cutting a release; rollout went from weeks to hours.
The clearest shift was qualitative. Product engineers stopped raising design questions in the first place: Storybook and the Chromatic snapshots answered what used to be a back-and-forth thread, and that time went back into their actual product work instead. The remaining year and a half went into holding that bar: closing the last stubborn 10%, growing the component count past 1000 variations as new screens needed them, and keeping the system multi-brand as TrueLayer's own brand requirements evolved.
Solving something similar?
I take on select engineering work like this.
What I'd do differently
- Start workshops and pairing alongside the first components, not once there was enough system to justify a session: adoption only accelerated once it became a recurring conversation, and that lesson cost several slow months up front.
- Build the Figma-to-tokens sync from day one. Design and code drifted apart a second time, the same failure mode this project existed to fix, in the gap before that check existed.
- Agree each component's deprecation date the moment its replacement ships, not months later: asking upfront is an easier conversation than asking after a team has resettled into the old one.
- Spread integration support across more than one person sooner. For the first few months I was the only route to an answer, so teams queued behind whoever I'd paired with that week.
- Make adoption numbers visible to every team continuously, not just brought to all-hands periodically: a live dashboard would likely have kept the same pressure a demo created.
Learning points
- Ship the substrate before the surface. Tokens went out alone, weeks before the first component, and gave every team a low-effort reason to touch the system early, which made every later component migration additive instead of a rewrite.
- Adoption is won face to face, not in a changelog. The adoption curve tracked pairing sessions and workshops far more tightly than it tracked how many components existed at any given point.
- Instrument before you need the argument. Wrapping every component in analytics from its first release meant adoption was never a debate, it was a chart anyone could pull up.
- A visible queue beats an implicit one. Once API-flexibility requests had one Jira board instead of scattered Slack threads, teams trusted a request would actually get answered, and stopped quietly forking the component locally instead.
- Deadlines move teams faster than invitations do. Nothing shifted the last holdout teams as reliably as an agreed sunset date on the components they were still running.