Design Systems That Scale: Lessons from 50+ Projects
How we built a component library that serves teams across different platforms.
A design system is more than a component library — it's a shared language between designers and developers. After building and maintaining design systems across 50+ projects at Vaarak, we've learned that the biggest challenges aren't technical. They're organizational: getting teams to adopt the system, keeping it up to date, and balancing consistency with flexibility.
This article shares our approach to building design systems that teams actually want to use, from foundational tokens to complex composite components, with practical examples and hard-won lessons along the way.
Layer 1: Design Tokens
Design tokens are the atoms of your design system — colors, typography, spacing, shadows, and motion values stored as platform-agnostic variables. They're the foundation that ensures a blue button on web, iOS, and Android are the exact same blue. We define tokens in a JSON format and generate platform-specific outputs (CSS custom properties, Swift constants, Kotlin values) through a build step.
{
"color": {
"brand": {
"primary": { "value": "#6366F1", "type": "color" },
"primary-hover": { "value": "#4F46E5", "type": "color" },
"secondary": { "value": "#06B6D4", "type": "color" }
},
"semantic": {
"success": { "value": "#10B981", "type": "color" },
"warning": { "value": "#F59E0B", "type": "color" },
"error": { "value": "#EF4444", "type": "color" },
"info": { "value": "#3B82F6", "type": "color" }
},
"neutral": {
"50": { "value": "#F8FAFC", "type": "color" },
"900": { "value": "#0F172A", "type": "color" }
}
}
}Name tokens by their purpose, not their value. Use 'color-brand-primary' instead of 'color-indigo-500'. This lets you rebrand without touching component code.
Layer 2: Primitive Components
Primitive components are the building blocks: Button, Input, Select, Badge, Avatar, Card. They're unopinionated about layout and business logic. The key principle is that each primitive should do one thing well, be fully accessible out of the box, and support composition through a consistent API.
We build primitives using Radix UI for accessibility and behavior, styled with Tailwind CSS and class-variance-authority (CVA) for variant management. This gives us unstyled, accessible primitives that we can theme consistently across projects.
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: "bg-brand-primary text-white hover:bg-brand-primary-hover",
secondary: "bg-slate-100 text-slate-900 hover:bg-slate-200",
outline: "border border-slate-300 hover:bg-slate-50",
ghost: "hover:bg-slate-100",
destructive: "bg-red-500 text-white hover:bg-red-600",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
}Layer 3: Composite Components
Composite components combine primitives into patterns that solve specific UI problems: DataTable, CommandPalette, FileUpload, DateRangePicker. These are where most of the complexity lives, and they're where design systems either shine or become a burden.
Our rule: composite components should be opinionated about structure but flexible about content. A DataTable component should handle sorting, pagination, and column resizing — but it shouldn't dictate what each cell looks like. Use render props or slot patterns for customizable areas.
Documentation as a First-Class Feature
The best component library in the world is useless if developers can't find what they need. Every component in our system has: a live interactive playground (Storybook), API documentation with prop tables, usage guidelines with do's and don'ts, accessibility notes, and copy-pastable code examples.
- Storybook stories for every component variant and state
- Chromatic visual regression tests to catch unintended UI changes
- Figma ↔ code mapping so designers and developers speak the same language
- Changelog with migration guides for breaking changes
- Usage analytics to understand which components are actually being used
Governance and Contribution
A design system that only one team controls will eventually fall behind. We use an "inner source" model: the core team maintains the primitives and enforces quality standards, but anyone can propose and contribute new components through a lightweight RFC process. Contributions go through design review, accessibility audit, and code review before being accepted.
“The measure of a design system's success isn't how many components it has — it's how often teams reach for the system instead of building custom solutions. If developers are constantly building outside the system, that's a signal that the system isn't meeting their needs.”
— Emily Nakamura, Vaarak Design Systems
Building a design system is a long-term investment. It takes 3-6 months to build the foundation, another 6 months for adoption to reach critical mass, and ongoing maintenance forever after. But the payoff is enormous: faster development, consistent user experience, and a shared vocabulary that bridges the design-engineering divide.
Emily Nakamura
Design Systems Lead