How to build a design system for a startup: a step-by-step recipe
Every startup rebuilds the same button five times before anyone notices. One screen has a 6px
radius, the next 8px; one green is #22c55e, another #21c65b because someone eyeballed it. None
of it is a bug — and that's exactly why it's dangerous. A design system is how you stop paying that
tax without slowing the team down.
This is the recipe I'd follow to build one for a product company. It's framework-agnostic on purpose: the ideas don't belong to React or Angular. At the end I show the same component in both, so you can see how little of a design system is actually about your framework.
A design system is not a component library
This is the first misconception to kill. A component library is a bag of buttons and inputs. A design system is the set of decisions — and the rules that keep them consistent — that a component library is just one output of. Four layers, from the ground up:
- Foundations → the raw decisions: color, spacing, typography, radii, motion. These are your design tokens.
- Components → foundations assembled into reusable pieces with a clear API.
- Documentation → how and when to use each piece, with real examples.
- Governance → who owns it, how it changes, how teams adopt it.
Skip the bottom and the top and you don't have a system — you have a folder of components that will drift within two sprints. The magic isn't the button; it's that everyone reaches for the same button and nobody argues about the radius.
A design system isn't what you build. It's what stops every team from rebuilding it.
Does your startup even need one?
Maybe not yet. A design system is leverage, and leverage only pays off at scale — more than one
product surface, more than a couple of engineers, a design language you expect to keep. If you're
three people shipping an MVP, a tokens.css file and a handful of shared components is your
design system, and that's the correct size.
The trap is the opposite: building the cathedral before there's a congregation. The same rule from the tokens post applies at every layer — a premature abstraction is over-engineering. Start with foundations (cheap, always worth it), add components as they repeat a third time, and only formalize governance when more than one team depends on you. Grow the system to match the pain, not the ambition.
The recipe
Here are the steps in the order I'd actually do them. Each one is small on its own; the value is in the sequence.
Step 1 · Foundations: design tokens
Everything starts here. A token is a design decision with a name — --color-accent, not
#22c55e. Organize them in three layers so a single change cascades everywhere: primitives
(what values exist), semantic (what they mean), and the optional component layer (a
configurable knob). I wrote the full breakdown in
Design tokens in 3 layers, so I won't repeat it — the key idea for a
system is that tokens are the contract. Every component consumes semantic tokens and nothing
else.
/* tokens.css — the single source, framework-agnostic */
:root {
/* primitive */
--green-500: #22c55e;
--neutral-950: #0c0a09;
--space-3: 0.75rem;
--radius-md: 0.75rem;
/* semantic — the contract components consume */
--color-accent: var(--green-500);
--color-text: var(--neutral-950);
--button-radius: var(--radius-md);
}If you want tokens that also feed native apps or emails, define them in JSON and generate the platform outputs with Style Dictionary. For a web-only startup, plain CSS custom properties are enough — don't add the build step until you have a second platform.
Step 2 · Make the tokens framework-agnostic
This is the step most teams skip, and it's the one that makes the rest cheap. Ship your tokens as CSS custom properties, imported once at the app root. Then React and Angular consume the exact same variables — no duplicated palettes, no per-framework theme objects to keep in sync.
// React entry — main.tsx
import './tokens.css';// Angular entry — angular.json "styles": ["src/tokens.css"]
// or a single @import in styles.cssThe payoff: your Button's color lives in var(--color-accent) regardless of framework. When design
changes the accent, you edit one line and both apps update. The tokens are the product; the
components are just typed wrappers around them.
Step 3 · Components and a good API
A component is only as good as its API. The question isn't "does it render a button" — it's "can someone use it correctly without reading the source?" A good component API has:
- Variants, not booleans-per-style. Prefer
variant="primary" | "ghost"overisPrimary+isGhost(which lets you set both and get nonsense). - A size scale that maps to tokens (
sm | md | lg), not arbitrary pixel props. - Explicit states: hover, focus, disabled, loading — designed, not accidental.
- Composition via children/slots, so the button doesn't need a prop for every possible label.
- Accessibility baked in: real
<button>semantics, visible focus ring,aria-*where needed. This isn't a nice-to-have; it's the difference between a component and a liability.
Design the spec once — variants, sizes, states, a11y — and it holds across frameworks. The implementation is a detail. For prior art on API shape, Radix UI (React) and the Angular CDK are the references I'd study.
Step 4 · Theming is a swap, not a rewrite
Dark mode and brand theming look scary until you realize they touch only the semantic layer. Primitives stay put, components never change — you just reassign what the semantic tokens point to.
:root[data-theme='dark'] {
--color-text: var(--neutral-50);
--color-bg: var(--neutral-950);
--color-accent: var(--green-400); /* lighter, to stay legible on dark */
}A Button that wrote var(--color-accent) re-themes itself with zero new lines of code. That's
the whole reason the token contract exists — theming is a variant of the semantic layer, not a
fourth layer. (I go deep on this in the tokens post.)
Step 5 · Documentation and discoverability
An undocumented component doesn't exist — people will rebuild it because they didn't know it was there. Storybook is the standard: it renders each component in isolation, shows every variant and state, and doubles as a visual test surface. For each component, document the three things people actually need:
- When to use it (and when to reach for something else).
- The props/inputs, with defaults.
- Do / Don't examples — the fastest way to prevent misuse.
Public systems like Shopify Polaris, GitHub Primer and IBM Carbon are worth studying not for their components but for how they document and govern them.
Step 6 · Governance and contribution
This is what separates a system that lasts from a repo that rots. Governance is unglamorous but decisive:
- Ownership: one team or person is accountable, even part-time. A system with no owner drifts.
- Naming conventions: agree how tokens and components are named before you have 200 of them. Nathan Curtis' Naming Tokens in Design Systems is the reference.
- Versioning: use semver so consumers know when a change is breaking.
- A contribution path: how does a product team propose a new variant? If the answer is "they can't," they'll fork, and your system fragments.
You don't need all of this on day one. You need it before the second team joins.
Step 7 · Distribution
When more than one app consumes the system, publish it as a versioned package (a private npm
package, or a monorepo workspace). One source of truth, semver-tagged, updated on a cadence. Until
then — a single app — a src/ui/ folder is perfectly fine. Packaging is a solution to a
multi-consumer problem; don't pay for it before you have that problem.
The same component in React and Angular
Here's the point of the whole post. Below is one Button spec — variant, size, disabled, driven
by tokens — implemented in both frameworks. Watch how the CSS and tokens are identical; only the
component syntax differs.
The shared styles, consumed by both:
/* button.css — shared, framework-agnostic, token-driven */
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
border: none;
border-radius: var(--button-radius);
cursor: pointer;
font: inherit;
}
.btn:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.btn--primary {
background: var(--color-accent);
color: var(--color-bg);
}
.btn--ghost {
background: transparent;
color: var(--color-text);
}
.btn--sm {
padding: var(--space-2) var(--space-3);
}
.btn--md {
padding: var(--space-3) var(--space-4);
}React — a typed functional component:
// Button.tsx
import './button.css';
type Variant = 'primary' | 'ghost';
type Size = 'sm' | 'md';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
}
export function Button({ variant = 'primary', size = 'md', className, ...rest }: ButtonProps) {
const classes = ['btn', `btn--${variant}`, `btn--${size}`, className].filter(Boolean).join(' ');
// Native <button> keeps semantics, focus and disabled for free
return <button className={classes} {...rest} />;
}Angular — a standalone component with signal inputs:
// button.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'button[appButton]',
standalone: true,
template: `<ng-content />`,
styleUrl: './button.css',
host: {
// Same classes, driven by the same token-based CSS
class: 'btn',
'[class.btn--primary]': "variant() === 'primary'",
'[class.btn--ghost]': "variant() === 'ghost'",
'[class.btn--sm]': "size() === 'sm'",
'[class.btn--md]': "size() === 'md'",
},
})
export class ButtonComponent {
// Same spec as React: variant + size, primary/md defaults
readonly variant = input<'primary' | 'ghost'>('primary');
readonly size = input<'sm' | 'md'>('md');
}<!-- usage -->
<button appButton variant="ghost" size="sm">Cancel</button>Two syntaxes, one design. The variants, the sizes, the focus ring, the disabled state, the accent color — all defined once in tokens and CSS. If you internalize one thing from this post, let it be this: a design system is mostly framework-agnostic, and the framework layer is the thin part.
Common mistakes
The failure modes are predictable. Watch for these:
- The component graveyard. Shipping 60 components nobody uses. Build the third repetition, not the hypothetical one.
- Raw values in components. A
#22c55ehardcoded in a button breaks theming silently. Consume semantic tokens, always. - Premature component tokens.
--button-x,--input-y"just in case." Add a knob when a real override needs it, not before. - Documenting last. If docs come after launch, adoption stalls and people rebuild. Document as you build.
- No governance. No owner, no naming rules, no versioning — and in six months it's a museum.
The checklist
The recipe in one scannable list — this is the part to keep open at work:
- Foundations — tokens in 3 layers, semantic layer as the contract.
- Agnostic — ship tokens as CSS custom properties, imported once.
- Components — variants over booleans, a size scale, real states, a11y baked in.
- Theming — a semantic swap, never a rewrite.
- Docs — Storybook + when-to-use + do/don't, written as you build.
- Governance — an owner, naming rules, semver, a contribution path.
- Distribution — a versioned package once there's a second consumer.
Right-size every step to your actual pain. A startup's best design system is the smallest one that keeps the team consistent — and grows only when it hurts not to.
Sources — go deeper
Everything above is a map; these are the territories. Each link is where I'd send someone to learn how it's actually implemented:
- Design tokens & naming — Nathan Curtis — Naming Tokens in Design Systems (the reference on structure and naming) · W3C Design Tokens Community Group (the official spec) · Style Dictionary (multi-platform token build).
- Component architecture — Brad Frost — Atomic Design (how to think about composing components into a system).
- Documentation — Storybook (the tooling) · real systems worth studying: Shopify Polaris, GitHub Primer, IBM Carbon.
- React implementation — Radix UI and shadcn/ui (accessible, unstyled/token-driven primitives done right).
- Angular implementation — Angular CDK and Angular Material (behavior and theming as reference).
- On this blog — Design tokens in 3 layers, the foundation this whole recipe is built on.
A design system isn't the button. It's the agreement that there's only one — and the discipline to keep it that way.