- Design System
- React
- TypeScrpt
- Frontend Architecture
Design system: the system comes before the components
by Bartek

Part 1 of a five part series on building a design system.
The proof of concept for our design system landed on 19 April 2023. The first Button shipped on 8 May 2024.
Thirteen months.
I have said that number out loud to a few people and watched them wince, so let me be clear about what it does not mean. It does not mean thirteen months of full-time work. The proof of concept sat untouched for most of 2023 while the team shipped product, which is what happens to every side quest in a company with a roadmap. Real work started in January 2024.
But even counting properly, from the first serious commit to the first component a product developer could use, is about four months of build with no components in it. That was deliberate, and it is the part of this project I would repeat without changing.
This is the first of five posts about building that design system: what we got right, what we got wrong, and what three years of twenty-odd people using it taught us. On pronouns, so the rest reads properly: I worked on the foundations and the tooling, so those are “I”. Most of the library was built by a team, so those are “we”.
The trap: starting with a Button
Every design system tutorial starts with a Button. It is the obvious first move. It is visible, it is finite, everyone knows what one looks like, and you get something on screen in an afternoon.
It is also the worst possible place to start, and the reason is not craft. It is that you do not know what components you need.
Not “you have not decided yet”. You cannot know. The list of components in a design system is downstream of the design vision, and the design vision moves. Ours moved constantly, because the product was not finished. Screens got redesigned. A page that needed a table in March needed a card carousel in July. Whole flows appeared that nobody had drawn when we started.
Early on, the library grew from a planned list. That worked while the list was mostly primitives everyone agreed on. Once we were maybe seventy or eighty percent of the way through, the source changed: new components started arriving attached to product work. Somebody builds a payments flow, finds they need something that does not exist, and the library grows. Our changelog from that period is full of entries like feat(payments) and feat(settings), all of them changes to the design system.
None of those arrived quietly. Each one was a conversation, usually several: with the designer about whether this was genuinely new or a near-duplicate of something we already had, with the team lead about whether it belonged in the library or the application, with a product manager about whether it was worth delaying a feature to do properly. A fair amount of convincing in both directions. I lost some of those arguments, and a couple of the things I lost turned out fine.
That process is slow and it is the point. The alternative is a library where anything anyone needed got added, which is a folder of components rather than a design system. But it means the roadmap for a design system past its first phase is not really yours to write.
The list shrinks too, which surprised me more. We removed a Button tone in a major version because it had become redundant. We narrowed a utility function’s signature because one of its three call shapes was a mistake we had been living with. A component library is not a set that only grows.
So if the component list is unknowable and unstable, what do you build first?
The thing underneath it that does not move.
What we actually built, in order
Here is the real sequence, taken from the commit history:
- Colour tokens (January 2024)
- The icon generation pipeline (February 2024)
- Documentation infrastructure (February 2024)
- Spacing and sizing scales (February 2024)
- Typography (April 2024)
- Button (May 2024)
Documentation infrastructure landed before typography. Before any component existed, there was a system that generated props tables from TypeScript types, waiting for props to document. That felt absurd at the time and it is the reason our documentation is still accurate three years later, which is the subject of the next post in this series.
The ordering has a logic. Each layer is something the layer above depends on and cannot easily change later. Colours first, because every component references them. Then the icon pipeline, because icons are a dependency of half the components and generating 769 of them by hand was never happening. Then documentation, because the cost of retrofitting docs onto an existing library is brutal. Then the spacing and sizing scales, because those are the vocabulary every layout uses. Then typography. Then, finally, something you can put on a page.
What “the system” actually is
The word “system” in “design system” does real work, and it is routinely ignored. A design system is not a component library. A component library is an output of one.
The system is the set of decisions that every component obeys without having to be told. Ours is about 750 lines of TypeScript, and after three years and a hundred thousand lines of library code, it is still about 750 lines.
A colour contract. 93 token names declared with no values attached. The values live in two theme files and nowhere else. The next post but one goes deep on this, so I will leave it there.
Closed scales. A spacing scale with fifteen steps. Eight corner radii. Three animation durations. Nothing between the steps exists, so nothing between the steps can be used.
Named dimensions instead of measurements. This one still gets a reaction. Our width accepts one of five values: auto, 100%, 100vw, fit-content, max-content. There is no arbitrary width in the API. Neither is there an arbitrary height. If a design calls for a 312 pixel card, that is a conversation, and having that conversation a hundred times over three years is how a product ends up looking like one product.
A z-index scale, and this one is a wart rather than a lesson. Ours tops out at 2147483647, the largest 32 bit signed integer, with dialog, wizard and inspector counting down from it. That is not a design decision. It exists so our overlays sit above the old design system’s, which we still run alongside ours, and it is on the list to delete once that is gone. A layering system needs about four levels: base, raised, overlay, and maybe one more. If your z-index scale has numbers in it that look like a joke, they are usually there for interoperability with something you are trying to kill.
Rem, never pixels. And here is one we got wrong. The early components used pixel values. In June 2024, well after several components existed, we converted the whole library to rem and added a lint rule that makes the string “px” a build error anywhere in the package. That retrofit was a bad afternoon that a decision in month one would have avoided. Not everything foundational gets recognised as foundational in time.
The thing that made it worth it
The payoff was not visible until much later, and it came in two forms.
The first was white labelling. We now run several deployments, each with its own theme. A theme is an implementation of the colour contract, so adding one is a matter of supplying values for names that already exist, with the compiler pointing at any you missed. No component knows what colour it is. They know they use surface.brand.
The second was a full product rebrand. From the design system’s side, it was largely a change of values in the theme files. The components did not need touching, because none of them had ever been told a colour. The genuinely hard part was proving that thousands of screens had changed correctly rather than accidentally, which is a whole post of its own later in this series.
Neither of those would have been possible if we had started with a Button and grown a token system around it as we went. You cannot retrofit a contract onto a hundred components that already hardcoded their colours. I have watched a team try.
The caveats
Two, and both matter.
This was built for our specific needs. A fintech product, a small number of applications, one design team, a strong requirement for visual consistency, and a business where an inconsistent interface reads as an untrustworthy one. If you are building a marketing site, or a product with three screens, or an agency template that fifty different clients will restyle, almost everything in this series is over-engineered for you. The right amount of system is proportional to how expensive drift is, and for us it was expensive.
This was built by hand. Effectively all of it. The first AI instruction files appeared in our repository in March 2025, twenty-three months after the project started and fourteen months after the real build began. The tokens, the scales, the typography, the icon pipeline and most of the component library predate any assistant touching the code. I mention it because “we built a design system” reads differently in 2026 than it did in 2023, and because what happened when we did introduce AI turned out to be the most interesting thing in the whole project. That comes later too.
The order is the argument
If there is one thing to take from this post, it is that the sequence is not a matter of taste.
Components are the visible part, so they are what gets asked for and what gets celebrated. But a component is a leaf. It is cheap to write, cheap to change, and cheap to throw away when the design moves. The scales, the token contract and the rules underneath are expensive to change and get more expensive every week, because everything built on top of them assumes they are stable.
So build the expensive things first, while they are still cheap. Then let the component list be as chaotic as the product needs it to be.
Next in this series: documentation that cannot rot, and why we built the documentation site before we had anything to document.