Architecting a design system as a multi-package ecosystem
A design system is not a component library. That distinction sounds pedantic until you have to maintain one, and then it becomes the single most important decision you make.
I learned it building Genesys UI, an open-source design system I architected for a company — and one of the most genuinely interesting things I've worked on. It started, like most do, as "a React component library," and grew into a dozen packages spread across as many repositories, each one a layer you could adopt on its own. This is the case for building it that way — and the bill that comes with it.
Layers, not a library
A component library answers one question: how do I render a button in React? A design system answers a harder one: what does a button look like, in any technology, and why? The "why" lives below the components, and if you bury it inside them, you can never get it back out.
In Genesys UI that "why" had a package of its own: a design system documentation Storybook, where the principles and rules of the system live, independent of any technology. Everything below it is those principles made concrete, in layers:
- Tokens — the design decisions as data. A theme generator (a CLI built
on Style Dictionary) compiles them into a published theme package —
theme-devowas the public one — in every format we needed. The generator is build-time tooling; the theme it produces is what you actually consume. - Base styles — a CSS reset and baseline, completely standalone: no tokens, no framework, usable under anything.
stylesandreact— the two ways to actually build with the system.stylesis plain CSS for hand-written HTML;reactis the component library. They are siblings, not a stack: each one needsbase-stylesand a theme to render correctly, and neither depends on the other.- Optional, à la carte —
icons(framework-agnostic: CSS or React, equally at home in plain HTML, Vue or Angular),react-hooks(generic React hooks, useful even outside the system), and a deliberately separate form builder for the one component too complex to live in the core.
Everything stays technology-agnostic right up until styles and react: the
design system, the tokens, the themes and the base styles know nothing about any
framework. And dependencies only ever point down — the ways you build lean on
the foundations, and nothing leans back up:
A token is just a value, so the same theme can feed a CSS variable, a Figma file
or an iOS build with equal ease. No need to reproduce it here: the
theme-devo repo holds both the source
schemas and the dist/ generated for each platform.
The payoff is concrete: a team on any stack — Vue, Angular, no framework at all —
can take base-styles, a theme and styles (or just the icons) and get the look
of the system without ever touching our React. You can only offer that if "the
look" was never trapped inside React to begin with.
One repo per layer — and sometimes several
Each layer lived in its own repository. That is the choice people argue about, so let me be honest about both sides.
What it buys you: independent release cadence (tokens can ship without forcing a React release), clear ownership boundaries, and — for an open-source project — real visibility, since each package stands on its own. It also forces the dependency direction to be honest: you cannot sneak an upward import across a repo boundary the way you can inside one folder.
It is also a hybrid, not a religion. Some layers were themselves monorepos — several related packages sharing a single repo — when the pieces shipped and versioned together. The grid that grew up inside the system eventually earned its own home as Grid Layout, precisely because it had stopped belonging to any one layer. The unit of separation is change, not dogma: things that change together live together; things that change apart get their own boundary. I pull that thread on its own in Monorepo or many repos? — there's really only one rule, and a lot of convincing reasons that aren't.
Treating CI and tooling as packages
The part I am most glad we did: we treated our own infrastructure as products.
A dozen repositories means a dozen CI pipelines and a dozen Storybook setups — and a dozen chances for them to drift apart. So the infrastructure became shared packages too:
gh-workflows— reusable CI workflows replicated across every package, andsb-addons— shared Storybook addons, so the docs looked and behaved the same everywhere.
The lesson generalises: in a multi-package system, your config is source code. If you copy-paste a workflow file into ten repos, you now maintain ten workflow files. Publish it once and consume it ten times instead.
The bill: versioning across a graph
None of this is free, and the cost is almost entirely coordination.
The moment you have more than one package, you have a dependency graph, and a
breaking change at the bottom has to ripple, in order, all the way up. Bump the
tokens in a new theme-devo and everything that consumes it has to follow:
styles and react both rebuild against the theme, and react-form-builder
follows react in turn. Get the order wrong and you ship a react that asks for
a theme version that does not exist yet.
(base-styles, standalone, doesn't move at all — which is exactly why keeping it
independent of the tokens earned its place.) With everything in one folder a
single commit fixes the whole graph atomically.
Split across repos, that same change becomes a choreographed sequence of releases.
This is the real tax of the multi-package approach, and anyone who tells you the
multi-repo split is "free modularity" has not paid it.
So the real decision is never "monorepo or many repos"; it's where to draw each line. Put everything in one place and the project balloons into a single giant with endless branches; split it too finely and you drown managing dependencies. The sweet spot is to keep together only what always changes together, and separate the rest.
icons is the lived example. We pulled it out of the main React monorepo because
an icon can be requested from outside React entirely — by styles (pure CSS), or
by someone else's Vue app. On its own it can evolve without touching anything else,
and a contributor can improve it without knowing the first thing about the react
package. That independence is real.
And yet it isn't total. Make a structural change to the icons — rename the suffix
the components use, say — and the ripple reaches every package that relates to them.
Thanks to composition the react library needs no code change… but its Storybook
examples, which use the icons, do. The coupling didn't disappear when we split the
repo; it moved out of the code and into the documentation. That's the part people
forget: independent packages still live in an ecosystem, and an ecosystem couples
through more than imports — through examples, docs, the shared story of how the
pieces fit together.
You are trading the convenience of atomic change for the freedom of independent release — a good trade for a design system meant to be consumed à la carte, a bad one for an app.
What held up, and where it goes next
The layering held up, and I'd reach for it again without hesitation. Separating the why (the design system and its tokens) from the what (the styles) from the how (React) is what made it a system instead of a pile of components — and inside the company it did exactly what it was built to do: teams shipped on it in production, across stacks that had nothing to do with each other, all speaking one visual language.
Where it goes next is coordination. A multi-package ecosystem lives or dies by how cleanly you can cut a coherent set of versions across the whole graph in a single motion, and that is precisely the kind of tooling that keeps getting better — the ground has moved a lot since we started, and it will move again. That frontier is what the open-source continuation exists to explore: a small group of the original authors keeping it alive, a full rebrand ahead, and plenty still left to build.
The architecture earned its keep, and I'd defend it again tomorrow. What makes it worth writing about isn't that it's finished — it's that it isn't: the shape is sound, the road ahead is open, and the best version of this system is still being written.