Building a Design System From Scratch: What We Learned the Hard Way
Design tokens, component libraries, and governance. How to build one that people actually use.
We built our first design system because our CEO opened the app on his phone and said, "Why does this button look different on every screen?" He wasn't wrong. We had 14 different shades of blue across the product and three distinct button styles that nobody could agree on. Something had to change.
What followed was an 18-month journey that was equal parts illuminating and humbling. We made mistakes that cost us months of work. We also discovered approaches that transformed how our entire engineering and design organization operates. This is that story — the real version, not the polished conference talk version.
Why Bother With a Design System?
Before we get into the how, let's talk about the why. Because building a design system is a significant investment, and "consistency" alone isn't a strong enough business case.
Here's what actually moved the needle for us:
- Development speed. Before the design system, building a new page meant making dozens of micro-decisions about spacing, typography, and component behavior. After? Developers assemble pages from pre-built, tested components. A page that took two days now takes half a day.
- Design-to-dev handoff. Designers and developers finally speak the same language. When a designer says "use the secondary action button with medium spacing," everyone knows exactly what that means. No more back-and-forth about padding values.
- Quality at scale. When you fix an accessibility issue in a component, it's fixed everywhere that component is used. When you have 47 custom implementations of a dropdown, good luck fixing them all.
- Onboarding. New team members become productive faster because the patterns are documented and consistent. They're not reverse-engineering tribal knowledge from the codebase.
Start With Tokens, Not Components
This was our biggest early mistake. We jumped straight into building a button component without first establishing the foundational layer: design tokens.
Design tokens are the atomic values that define your visual language. Colors, spacing scales, typography, border radii, shadows, breakpoints. They're the DNA that makes your system feel cohesive.
Color Tokens
Don't just name your colors by their hue. blue-500 tells you nothing about purpose. Instead, create a two-tier system:
- Primitive tokens — The raw palette.
blue-50throughblue-900,gray-50throughgray-900, etc. - Semantic tokens — Purpose-driven aliases.
color-action-primary,color-background-danger,color-text-muted. These reference primitive tokens but communicate intent.
Semantic tokens are what make theming possible. Want a dark mode? Remap your semantic tokens to different primitives. Want a brand refresh? Same thing. The components never need to change.
Spacing Scale
Pick a base unit and stick to it. We use 4px as our base, which gives us a scale of 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, and 96. Every margin, padding, and gap value in the system comes from this scale.
It sounds restrictive, and it is — intentionally. Constraints breed consistency. When a developer can't use 13px of padding (because it's not on the scale), they use 12 or 16, and the result looks like it belongs in the same product.
Typography Scale
Define your type hierarchy once and reference it everywhere. We settled on seven levels: display, heading-1 through heading-4, body, and caption. Each level specifies font family, size, weight, line height, and letter spacing. No one-off font sizes allowed.
Building the Component Library
With tokens in place, we moved on to components. Here's where things got interesting.
The "80/20" Approach
You don't need to build every component before launching your design system. We started with the components that appeared on every screen: Button, Input, Select, Card, Modal, and Typography. These six components covered roughly 80% of our UI surface area.
Everything else — data tables, date pickers, multi-select dropdowns — came later, driven by actual need rather than speculative completeness. We've seen teams spend six months building 50 components that nobody uses. Don't be that team.
API Design Matters Enormously
The API of your components — the props they accept, the patterns they follow — is arguably more important than their visual appearance. A well-designed API makes the right thing easy and the wrong thing hard.
Some principles we follow:
Composition over configuration. Instead of a Button component with 15 boolean props (isLoading, hasIcon, isFullWidth...), use composition. A button contains children — those children can be text, an icon, a spinner, or any combination. The button doesn't need to know or care.
Controlled by default. Form components should be controlled (state managed by the parent) by default, with an uncontrolled option via a defaultValue prop. This matches React conventions and makes testing straightforward.
Forwarded refs and spread props. Every component should forward a ref and spread unknown props to the root element. This ensures components work with third-party libraries (tooltip libraries, form libraries, animation libraries) without needing escape hatches.
Accessibility From Day One
Not day two. Not "when we have time." Day one.
Every component in our system meets WCAG 2.2 AA standards. Keyboard navigation, screen reader support, focus management, color contrast — these are table stakes, not nice-to-haves.
The good news: when accessibility is built into the design system, individual feature teams don't have to think about it for common patterns. The button already handles focus states. The modal already traps focus. The form inputs already have proper ARIA attributes. Accessibility becomes the default rather than an afterthought.
We run automated accessibility tests (using axe-core) on every component as part of our CI pipeline. If a PR introduces an a11y violation, it doesn't merge.
Documentation That People Actually Read
A design system without documentation is just a component library that nobody knows how to use. We invested heavily in documentation, and it paid for itself many times over.
What Good Documentation Looks Like
For each component, we provide:
- Live examples — Interactive playgrounds where developers can modify props and see the result. We use Storybook for this, and it's been fantastic.
- Usage guidelines — When to use this component (and when to use something else). "Use a Modal for confirmations. Use a Sheet for complex forms. Use a Toast for non-blocking notifications."
- Do/Don't examples — Visual examples of correct and incorrect usage. These are surprisingly effective at preventing misuse.
- Prop tables — Auto-generated from TypeScript types. Always up to date because they're derived from the source code.
- Migration guides — When we make breaking changes (rare, but it happens), we provide step-by-step migration instructions with codemods where possible.
The Decision Log
This was a game-changer for us. Every significant design decision gets documented with the reasoning behind it. "Why does our Select component close on selection in single-select mode but stay open in multi-select mode?" There's an entry for that, with user research data backing the decision.
When someone new joins and asks "why is it done this way?", the answer is documented. No more tribal knowledge.
Governance: The Hardest Part
Building the design system is the easy part. Keeping it alive, evolving, and adopted is where most systems fail.
The Contribution Model
We use a federated model. A core team of two designers and two developers maintains the system, but anyone can contribute. Contributions follow a clear process:
- Open a proposal issue describing the need
- Core team reviews and provides feedback within one week
- Contributor builds the component following system conventions
- Core team reviews the implementation
- Component is added to the system with full documentation
The key is the one-week response time. If contributors feel like their proposals disappear into a black hole, they'll stop contributing and build custom solutions instead.
Versioning and Releases
We follow semantic versioning strictly. Patch releases for bug fixes, minor releases for new components and features, major releases for breaking changes. Breaking changes happen at most once per quarter, with at least a six-week deprecation period.
Every release has detailed changelogs. Every breaking change has a codemod or detailed migration guide. Make upgrading easy, and teams will stay current. Make it hard, and they'll pin an old version forever.
Measuring Adoption
How do you know if your design system is actually being used? We track a few metrics:
- Coverage score — What percentage of UI elements in the product come from the design system versus custom code? We measure this automatically via lint rules.
- Contribution rate — How many contributions come from outside the core team? A healthy system has a steady stream of external contributions.
- Support ticket volume — How many questions are teams asking about the system? A declining trend means the documentation is working.
Mistakes We Made
In the spirit of learning from our screw-ups:
We tried to be too flexible. Early on, every component had a className override prop and style customization options. Teams used these extensively, which meant they were technically using the design system but their UIs looked nothing alike. We eventually tightened the API — less flexibility, more consistency.
We didn't involve developers early enough. The first version was designed entirely by the design team. When developers tried to implement the specs, they found dozens of edge cases that hadn't been considered. Now, designers and developers collaborate from the first sketch.
We underestimated the documentation effort. We spent 60% of our time building components and 40% documenting them. In hindsight, those numbers should have been closer to 50/50.
We built in isolation. For three months, the design system team worked in a silo. When we emerged with a "finished" system, teams had already built their own solutions and weren't eager to switch. Now, we ship incrementally and get adoption feedback continuously.
Was It Worth It?
Absolutely. Our design-to-development cycle is 40% faster. Accessibility issues have dropped by 70%. New developer onboarding time (to the point of productive UI work) went from two weeks to three days. And our product finally looks like one product rather than a collection of features built by different teams in different years.
But it took longer than we expected, cost more than we budgeted, and required more organizational change management than any purely technical initiative we've undertaken. If you go in with eyes open about the investment, the returns are absolutely there.
Thinking about building a design system for your product? We've been through the process and can help you avoid the detours we took.
Comments
No comments yet. Be the first to share your thoughts!
Need Expert Software Development?
From web apps to AI solutions, our team delivers production-ready software that scales.
Get in Touch
Leave a comment