How to set up a design system without spending six months on it
Most articles about design systems stop at the philosophy: shared language, single source of truth, scalability. All true, all useless on a Monday morning when you have a product to ship. This guide is the opposite. It is the actual workflow a team of two to five people can run in a single sprint, from the first design token to a versioned component library installed with npm install.
At Pixelseed we have built this exact pipeline for client products and for our own apps. Below is the condensed version: the folder structure, the naming rules, the Figma setup, the Storybook configuration and the sync strategy that keeps design and code from drifting apart three months later.
What a design system actually is (the 60 second version)
A design system is three layers stacked on top of each other. If you are missing one, you do not have a design system, you have a UI kit.
| Layer | What it contains | Where it lives |
|---|---|---|
| Foundations | Design tokens: color, spacing, radius, typography, shadows, motion | Figma Variables + a JSON file in Git |
| Components | Button, Input, Select, Card, Modal, Toast, Table | Figma components + a React package |
| Rules | Usage guidelines, accessibility, contribution and versioning process | Storybook docs pages |
The trap small teams fall into is starting with layer two. You design forty beautiful components, then discover every one of them hardcodes #2F6BFF and nothing can be themed. Start with tokens.

The one sprint plan
Here is how we split a five day sprint. Scope is deliberately brutal: eight components, not forty.
| Day | Goal | Deliverable |
|---|---|---|
| 1 | UI inventory and token definition | tokens.json + Figma Variables |
| 2 | Repo scaffolding and build pipeline | Package that compiles and exports |
| 3 | Core components in React | Button, Input, Select, Checkbox |
| 4 | Composite components + Storybook | Card, Modal, Toast, Table + docs |
| 5 | Publish, document, consume | v0.1.0 on the registry, used in the app |
Step 1: Run a UI inventory before you design anything
Open your product. Screenshot every screen. Paste them in one Figma board. Then count.
- How many distinct button styles exist? (Teams usually find 9 to 15.)
- How many shades of grey?
- How many border radius values?
- How many font sizes?
This audit is the single most persuasive artifact you will produce. It turns “we should do a design system someday” into “we have 14 greys and nobody knows which one is correct”. It also gives you the real scope: the components that appear on more than three screens are your sprint backlog. Everything else waits.
Rule of thumb: a component earns a place in the library when it is used in at least three different contexts. Below that, keep it local to the feature.
Decide the scope of version 0.1.0
Eight components cover roughly 70 percent of a typical SaaS interface:
- Button (with variants and sizes)
- Input / TextField
- Select
- Checkbox and Radio
- Card
- Modal / Dialog
- Toast / Alert
- Table (or DataList)

Step 2: Define your design tokens in three tiers
This is the part that determines whether your design system survives a rebrand. Use a three tier token architecture.
| Tier | Example | Who uses it |
|---|---|---|
| 1. Primitive (raw values) | blue-500: #2F6BFF |
Nobody, except tier 2 |
| 2. Semantic (intent) | color-action-primary: {blue-500} |
Designers and app developers |
| 3. Component (optional) | button-bg-primary: {color-action-primary} |
The component implementation only |
Tier 3 is optional for a small team. Tiers 1 and 2 are not. The reason is simple: when the brand changes, you edit tier 1 and the whole product follows. When you add dark mode, you remap tier 2 and nothing else moves.
Naming conventions that will not embarrass you in a year
Pick a pattern and never deviate. We use category-property-variant-state, all lowercase, hyphen separated:
color-bg-surface
color-bg-surface-hover
color-text-primary
color-text-muted
color-border-default
color-action-primary
color-action-primary-hover
color-feedback-danger
space-100 (= 4px)
space-200 (= 8px)
radius-md
font-size-body-m
shadow-raised
duration-fast
Three rules that save the most pain:
- Never put the value in the name.
space-8is a lie waiting to happen when you rescale the grid. Use a numeric scale (100,200,300) or t-shirt sizes (xs,sm,md). - Never name a semantic token after a color.
color-blue-buttonbreaks the day the button turns green. Usecolor-action-primary. - Use the same names in Figma and in code. If the Figma variable is
color/action/primary, the CSS variable is--color-action-primary. No translation layer, no mental overhead.
Where the tokens live
Store them as a JSON file in your repo, following the W3C Design Tokens format:
{
"color": {
"blue": { "500": { "$type": "color", "$value": "#2F6BFF" } },
"action": {
"primary": { "$type": "color", "$value": "{color.blue.500}" }
}
}
}
Then compile it with Style Dictionary into the outputs you need: CSS custom properties, a TypeScript object, and a Tailwind theme if you use it. One source, many targets.
:root {
--color-action-primary: #2F6BFF;
--space-200: 8px;
--radius-md: 8px;
}
Step 3: Mirror the tokens in Figma with Variables
In Figma, create Variable collections that match your tiers:
- A collection named
primitiveswith a single mode, containing your raw ramps. - A collection named
semanticwith two modes:lightanddark, where every variable aliases a primitive. - Apply only semantic variables inside components. Primitives are never used directly on a layer.
Use slash notation for grouping (color/action/primary) so Figma builds a clean folder tree that maps one to one onto your JSON structure.
To move tokens between Figma and Git, you have two realistic options for a small team:
- Tokens Studio plugin, configured to push and pull a
tokens.jsonfile from a dedicated branch of your repo. Designers push, a pull request is opened, a developer reviews and merges. This is our default. - Figma Variables REST API with a small script in CI that exports variables nightly and opens a PR when something changed. More setup, less plugin dependency.
Pick one direction as the source of truth. Ours is Figma for token values and Git for token structure. Mixed ownership in both directions is how teams end up with conflicting color scales.

Step 4: Scaffold the component library repo
Keep it boring. A single package is enough until you have a second consumer app.
packages/ui/
src/
tokens/
tokens.json
build.js # Style Dictionary
generated/ # css + ts output, gitignored or committed
components/
Button/
Button.tsx
Button.module.css
Button.stories.tsx
Button.test.tsx
index.ts
hooks/
utils/
index.ts # public barrel export
package.json
tsconfig.json
vite.config.ts # library mode build
.storybook/
Toolchain we recommend as of late 2026:
- React 19 with TypeScript in strict mode
- Vite in library mode for the build, outputting ESM plus type declarations
- Radix UI primitives (or React Aria) for behaviour and accessibility, so you only write the styling
- CSS Modules or Tailwind v4, both driven by the same CSS variables from your tokens
- Storybook for docs, Vitest + Testing Library for tests
- Changesets for versioning and changelogs
Do not build your own headless primitives in sprint one. A correct accessible Select with keyboard navigation, typeahead and focus trapping is a week of work on its own.
Component API conventions
Agree on these before the first component is written. They are what make a library feel coherent.
| Convention | Rule |
|---|---|
| Visual variants | variant="primary" | "secondary" | "ghost" | "danger" |
| Sizes | size="sm" | "md" | "lg", same scale on every component |
| Booleans | Positive phrasing: isLoading, isDisabled, never notEnabled |
| Native props | Always extend the native element props and forward the ref |
| Escape hatch | Accept className, never accept a raw style object |
| Naming parity | The Figma component property name equals the React prop name |
That last row is underrated. If a designer sets Variant = Ghost in Figma and the developer writes variant="ghost", handoff conversations shrink to nothing.
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
leadingIcon?: React.ReactNode;
}
Step 5: Document with Storybook (and make the docs the product)
Storybook is not a component playground, it is your published documentation site. Treat it that way.
For every component, ship:
- A Docs page written in MDX with a one paragraph description and a “when to use / when not to use” block
- A default story showing the canonical usage
- One story per variant and per state, including loading, disabled, error and long content overflow
- An autodocs props table generated from your TypeScript types
- An accessibility check via the a11y addon, failing the build on violations
export default {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
parameters: {
design: { type: 'figma', url: 'https://figma.com/file/.../Button' }
}
} satisfies Meta<typeof Button>;
Add the Figma design addon so each story embeds the matching Figma frame. Now the component page shows the design and the implementation side by side, in the same tab. Drift becomes visible instead of theoretical.
Deploy Storybook on every merge to main (Chromatic, Netlify or Vercel all work). The URL goes in your README, your Figma cover page and your team channel topic. Documentation that requires a local server does not get read.
Write the three sections designers actually read
- Usage: when to pick this component over a similar one. “Use Toast for non blocking confirmation. Use Modal when the user must decide before continuing.”
- Content guidelines: button labels start with a verb, maximum three words, sentence case.
- Do and do not: two screenshots side by side. This single block prevents more misuse than a page of prose.

Step 6: Publish the component library
A design system that is copy pasted between projects is not a system. Publish it.
- Scope the package:
@yourcompany/ui. - Publish to GitHub Packages or a private npm registry. A private registry is fine; you do not need to open source anything.
- Set
reactandreact-domas peerDependencies, never dependencies. - Export ESM with type declarations and set
"sideEffects": ["*.css"]so bundlers tree shake correctly. - Use Changesets: every PR that touches the library includes a changeset file, CI opens a release PR, merging it publishes and writes the changelog.
Semantic versioning rules your team should agree on in writing:
| Change | Bump |
|---|---|
| New component, new optional prop | minor |
| Bug fix, token value tweak | patch |
| Renamed or removed prop, renamed token | major |
| Visual change that alters layout in apps | major |
Deprecate before you delete. Mark the prop with a JSDoc @deprecated tag, log a console warning in development, and remove it one major version later.
Step 7: Keep Figma and code in sync for real
This is where most design systems die, usually around month three. Four mechanisms keep ours alive.
1. Tokens flow one way, automatically
Designers edit Figma Variables. A plugin or CI job pushes tokens.json to a branch. Style Dictionary rebuilds the CSS. Nobody retypes a hex code. If a value is wrong in the app, it is wrong in Figma too, and that is a feature.
2. Figma components mirror React props exactly
Use Figma component properties with the same names and the same allowed values as your TypeScript union types. Build the variant matrix in Figma, not forty detached frames.
3. Code Connect for the handoff
Figma’s Code Connect lets you attach a real code snippet to a Figma component, so Dev Mode shows the actual import and props instead of generated CSS. Setting it up for eight components takes about an hour and removes the most common source of reimplementation.
figma.connect(Button, 'https://figma.com/file/.../Button', {
props: {
variant: figma.enum('Variant', { Primary: 'primary', Ghost: 'ghost' }),
label: figma.string('Label')
},
example: ({ variant, label }) => <Button variant={variant}>{label}</Button>
});
4. Visual regression tests
Run Chromatic or Playwright screenshot tests on every pull request. When a token change shifts a button by two pixels across nineteen stories, you see it before your users do.
5. A recurring 30 minute sync
Not glamorous, but necessary. Every two weeks: review the components added locally in feature branches, decide which ones get promoted into the library, and kill the duplicates. This meeting is the actual governance process for a small team. You do not need a design system council.

Common mistakes we see in audits
- Building forty components before shipping one. Publish v0.1.0 on Friday with eight components. Momentum beats completeness.
- No semantic layer. Components reference
blue-500directly, so dark mode requires rewriting every file. - Over configurable components. A Button with 23 props is a styling framework, not a design decision. Constraints are the point.
- Documentation inside Figma only. Developers do not open Figma to check a prop name. Put it in Storybook.
- No owner. Assign one person as maintainer, even at 10 percent of their time. Shared ownership means no ownership.
- Skipping accessibility at the start. Retrofitting focus management and ARIA into thirty components costs ten times more than doing it in the first eight.
Your day 5 checklist
- ☐
tokens.jsoncommitted, compiling to CSS variables and TypeScript - ☐ Figma Variables collections mirroring the token names, light and dark modes
- ☐ Eight React components with typed props, forwarded refs and keyboard support
- ☐ One Storybook story per variant and per state, a11y addon passing
- ☐ Storybook deployed at a public or internal URL
- ☐ Package published as
@yourcompany/[email protected] - ☐ At least one real screen in the product refactored to use it
- ☐ Contribution guide in the README: how to propose, build and release a component
That last item matters more than it looks. A design system is only adopted when contributing to it is easier than working around it.
FAQ
How long does it take to set up a design system?
A functional first version with tokens, eight components, Storybook documentation and a published package takes one focused sprint for a team of two to three. A mature system covering an entire product is a continuous effort measured in quarters, but you should never wait for maturity before shipping version one.
Should I start in Figma or in code?
Start with tokens, in both at once, during the same session. The designer creates the Figma Variables while the developer writes tokens.json with identical names. Two hours of pairing on day one prevents months of translation work later.
Do I need a monorepo?
Not for a first version. A single ui package published to a private registry is enough. Move to a monorepo with pnpm workspaces and Turborepo when you have at least two consumer applications or you split tokens, icons and components into separate packages.
Should I build components from scratch or use a library?
Build on unstyled primitives such as Radix UI or React Aria, and own the styling layer completely. You get correct accessibility and keyboard behaviour for free while keeping full visual control. Building fully styled libraries like MUI into a custom design system usually ends in a fight with the theming layer.
How do I convince management to invest a sprint in this?
Use the UI inventory from step 1. Count the number of duplicate button implementations and estimate how long it takes to apply one visual change across them today. Then compare that to changing a single token. The payback is normally visible within two or three design iterations.
Can AI tools generate my design system?
AI assistants are genuinely useful for the repetitive parts: generating color ramps, writing story files, drafting prop documentation, converting a token file to another format. They are not useful for the decisions that matter, which are your naming conventions, your constraints and your governance. Automate the typing, not the thinking.
What if we already have a messy codebase?
Do not rewrite. Introduce the library, then adopt it one screen at a time, starting with the screens that change most often. Add an ESLint rule that blocks raw hex values in new code. Legacy components get migrated when they are touched for another reason.
Need help getting it off the ground?
At Pixelseed we build design systems and React component libraries for product teams that need to ship, not to theorise. If you want a second pair of hands for the first sprint, or an audit of a system that has started to drift, get in touch and we will walk through your UI inventory together.