Design Systems for Developers: Building UI that scales with your codebase
How to build a consistent, maintainable design system from scratch — covering tokens, component APIs, and the handoff between design and code.
Every project starts clean. You pick a font, settle on a primary color, and write your first button. Three months later you have seven slightly different button variants, four shades of gray that all mean "secondary text," and a design file that diverges more from the codebase with every sprint. Sound familiar?
A design system is the answer — but most tutorials approach it from the designer's side. This one is for developers: the people who actually write the CSS, define the component props, and live with the consequences.
Start with tokens, not components
The instinct is to build components first. Resist it. A button built before you've defined your spacing scale will just be a button — not a piece of a coherent system. Design tokens are the atoms: named values for color, spacing, typography, and motion that everything else references.
In CSS, this means custom properties. In code, it might be a constants file or a theme object. The key insight is that a token like `--color-text-secondary` is far more durable than `#606055`. When you rebrand or support dark mode, you change the token — not every component that uses the color.
Define your token hierarchy before writing a single component. Color tokens should flow from primitive (the raw hex) to semantic (what it means). `--gray-500` is a primitive. `--color-text-muted` is semantic. Components reference semantic tokens only.
Component APIs are contracts
When you write a component, you're defining an interface. The props are a contract with everyone who uses it. Design that contract deliberately. A `Button` with a `variant` prop that accepts `"primary" | "ghost" | "danger"` is far easier to consume — and constrain — than one that accepts arbitrary `className` overrides for everything.
The "escape hatch" problem is real: if you don't give people a safe way to deviate, they'll find an unsafe one. Build in a `className` prop for layout-level concerns (margin, width) but never expose it as a styling free-for-all. Document what's intentional and what isn't.
Composition beats configuration. A `Card` that accepts a `header` slot and a `footer` slot scales better than a `Card` with 14 boolean props. Think about the natural units of variation and model them explicitly.
The design-to-code handoff
The most common failure mode isn't bad components — it's drift. A designer updates a color in Figma, a developer ships a slightly different shade, and six months later you have two parallel realities. The handoff process needs to be a loop, not a one-way street.
The most practical fix is to make tokens the canonical source of truth and share them across Figma and code. Tools like Tokens Studio (for Figma) can sync directly to a JSON file that feeds your CSS custom properties. When the designer changes a token, the developer pulls the change — no interpretation required.
If that tooling investment isn't feasible, at minimum establish a convention: every design decision has a named token, and the name is the same in Figma and in code. The discipline of naming is what prevents "that blue" from becoming five different blues.
Scaling without bureaucracy
A design system should feel like infrastructure, not governance. The moment it feels like you need approval to add a component, you've over-engineered it. Keep the core small and opinionated; let the consuming codebases extend it for their specific needs.
Version it. Changelog it. When you make a breaking change to a component API, say so explicitly. The overhead of maintaining a `CHANGELOG.md` is tiny compared to debugging why three different teams' UIs broke after a library update.
The goal isn't consistency for its own sake — it's speed. A well-built design system means the tenth feature ships faster than the first, because the decisions are already made. That's the return on the investment.