Design a Component Library / Design System
Designing a component library or design system is a classic frontend system design question. It evaluates your ability to build scalable, consistent, and highly maintainable UI foundations. Interviewers use this topic to assess your technical depth across styling architectures, API design ergonomics, accessibility compliance, and code governance across multi-repository organizations.
1. What the Interviewer Is Testing
When an interviewer asks you to design a component library, they aren't just checking if you know how to build a button or a dialog box. They are evaluating your capacity as a system architect. Specifically, they look for:
- API Design Thinking (Developer Experience/DX): Can you build clean, predictable component APIs that other developers love using? Do you support ref forwarding, standard HTML attribute spreading, and logical prop grouping?
- Consistency & Scale: How do you enforce design constraints (like typography, layout grids, and brand colors) across hundreds of pages and multiple distinct teams?
- Token System Understanding: Do you know how design values transition from a designer’s canvas (e.g., Figma) to code across multiple targets (Web, iOS, Android, email templates)?
- Accessibility (a11y) by Default: Do your components natively handle screen readers, keyboard focus management, focus-traps, and visual contrast guidelines?
- Governance and Adoption Strategy: A design system is useless if product teams refuse to adopt it. How do you manage versioning, distribute updates, handle breaking changes, and support custom variations without code duplication?
2. How to Open Your Answer (The First 2 Minutes)
Do not start coding or drawing immediately. Start by asking clarifying questions to define the boundaries of the design system.
Say something like:
"Before I design the architecture, I want to clarify the organizational context. Are we building a greenfield design system for a single product, or are we unifying a fragmented product ecosystem with existing codebases? I will assume we are building a unified system across multiple web properties.
Who are the primary consumers? Are they internal developers within our company, or is this an open-source library used by external teams? I'll design this web-first for internal developers, prioritizing extensibility and solid component boundaries.
What platforms must we support? Is this web-only (React, Vue, or Web Components), or must we support native mobile applications (React Native, iOS Swift, Android Jetpack Compose)? I will design a cross-platform design token pipeline that compiles to all platforms, while focusing our UI component layer on React (Web).
Finally, what is the styling model? Do we require runtime style customization, or can we compile styles ahead of time? I will design a system that uses CSS custom properties (variables) to enable runtime theme-switching (such as light/dark mode or multi-tenant branding)."
3. The 7-Step Answer Framework
Walk through this framework systematically to demonstrate structure, architectural clarity, and depth.
Step 1: Requirements Gathering (Functional & Non-Functional)
- Functional Requirements:
- Unified Foundation: A single source of truth for design parameters (tokens).
- Core UI Component Library: A suite of highly reusable UI components (Buttons, Modals, Autocompletes, DataTables).
- Theming: Support dark mode, high-contrast mode, and white-labeling (brand swapping).
- Interactivity & Accessibility: WCAG 2.1 AA compliance (keyboard control, screen reader semantics).
- Non-Functional Requirements:
- Tree-shakability: Product bundles must only import components they actually use.
- Low Overhead: Minimal runtime rendering performance impact and styling compilation times.
- Type Safety: Complete TypeScript definitions for all props, slots, and events.
- Governance & Support: Clear guidelines for component updates, migrations, and bug reporting.
Step 2: The Foundation Layer — Design Tokens
Design tokens are the atomic decisions of a brand (colors, typography, spacing, border-radius, elevation). Never hardcode values. Instead, implement a three-tier design token hierarchy:
- Global / Tier 1 Tokens (System/Brand): Abstract, raw values that represent the brand's complete palette.
- Example:
color-blue-500: #3B82F6,spacing-4: 16px.
- Example:
- Alias / Tier 2 Tokens (Semantic): Assign meaning to the raw colors based on context. This is where dark mode is defined.
- Example:
color-bg-primary: color-blue-500(in light mode) orcolor-bg-primary: color-gray-900(in dark mode).
- Example:
- Component / Tier 3 Tokens (Scoped): Scoped to specific components. This allows changing a single component's styling without impacting others.
- Example:
button-bg-primary-default: color-bg-primary.
- Example:
Token Pipeline Architecture
Use Style Dictionary (or a similar build-time tool) to transform raw JSON token files into platform-specific outputs:
Step 3: Primitive Components (Flexible, Low Opinion)
Primitive components are the simple building blocks of your UI. They do not hold state related to business logic.
- Examples:
Box(a layout utility),Text,Button,Input,Avatar. - Key Design Principle: Keep them highly configurable but low on opinion. A primitive button shouldn't handle analytics triggers; it should just render a button and bubble up standard click events.
- API Ergonomics:
- Use Ref Forwarding: Always use
React.forwardRefto pass the DOM ref back to the consumer. This is crucial for custom animation libraries, positioning overlays, or focus management. - Use HTML Attribute Spreading: Allow developers to pass native attributes (like
aria-label,type="submit", ordisabled) seamlessly via...restprops. - Ensure Type Extensibility: Extend native HTML element types (e.g.,
React.ButtonHTMLAttributes<HTMLButtonElement>).
- Use Ref Forwarding: Always use
Step 4: Composite Components (Opinionated, Assembled)
Composite components are built by composing multiple primitives together. They represent common, complex patterns across your product.
-
Examples:
DatePicker,DataTable(with built-in sorting, filtering, and virtualization),Form(input controls + validation messaging),Cardlayouts. -
Key Design Principle: High opinion, low customizability at the styling layer, but high customizability at the data layout layer.
-
API Pattern: Use the Compound Component Pattern to allow consumers to customize the DOM structure without writing complex configuration objects.
For example, instead of a configuration-heavy tabs component:
// ❌ Bad: Monolithic API - hard to extend or style custom slots
<Tabs data={tabData} activeKey="tab1" onTabChange={handleTabChange} />
// ✅ Good: Compound Component API - highly flexible layout
<Tabs defaultValue="tab1">
<Tabs.List aria-label="Project Actions">
<Tabs.Trigger value="tab1">Details</Tabs.Trigger>
<Tabs.Trigger value="tab2">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Project detail panel...</Tabs.Content>
<Tabs.Content value="tab2">Project settings panel...</Tabs.Content>
</Tabs>Step 5: Theming System
A modern theme system must support both static customization (compile-time) and dynamic switching (runtime).
- CSS Custom Properties (Variables): The gold standard for web theme systems. By declaring semantic variables on the
:root, switching themes is as simple as toggling a class on the<html>or<body>element. - CSS-in-JS vs CSS Modules:
- CSS-in-JS (e.g., Styled Components, Emotion): Provides excellent developer ergonomics and dynamic prop-based styling, but incurs a runtime rendering penalty and complicates Server-Side Rendering (SSR) / React Server Components (RSC) hydration pipelines.
- Utility/Precompiled CSS (e.g., Tailwind, CSS Modules, Vanilla Extract): Zero runtime styling overhead, perfect for RSC compatibility, but requires compilation setups.
- Hybrid Recommendation: Use Tailwind CSS or CSS Modules combined with CSS Custom Properties. Define design tokens as CSS variables, and map Tailwind utility classes directly to those variables.
Step 6: Accessibility (a11y) Baked In
Accessibility cannot be bolted on at the end; it must be designed into the foundation.
- Use Headless Primitives: Don't reinvent keyboard navigation, focus management, and ARIA state updates. Build your styling layer on top of tested headless libraries like Radix UI Primitives, React Aria, or Headless UI.
- Focus Management:
- Focus Trapping: For modals, sheets, and popovers, trap keyboard focus within the overlay. Focus must not escape to elements underneath.
- Focus Restoration: When a modal closes, return focus to the exact button that triggered it.
- Keyboard Interactivity:
- Buttons react to
SpaceandEnter. - Menus, Select dropdowns, and Tabs navigate using
Arrow Keys. - Modals and dropdowns dismiss on
Escape.
- Buttons react to
- ARIA Roles: Provide proper semantics (
role="dialog",role="tablist",role="combobox"), along with dynamic state hooks (aria-expanded="true",aria-invalid="true").
Step 7: Documentation, Versioning & Adoption
The success of a design system is measured by its adoption rate, not its component count.
- Documentation Site (Storybook / Custom Docusaurus):
- Provide live playgrounds using react-live or Storybook controls.
- Document the "Why" (Design guidelines, Do's and Don'ts).
- Clear accessibility guidelines for each component.
- Versioning (Monorepo Approach):
- Use a monorepo setup (using tools like Turborepo or Nx with Changesets).
- Deploy components from a single package (
@company/ui) for simplicity, or multi-package structures if consumers require strict bundle controls.
- Deprecation and Migration Governance:
- Avoid sudden breaking changes. Mark old features as deprecated using JSDoc
@deprecatedwarnings and runtime console notices in development. - Write Codemods (using AST transformation libraries like
jscodeshift) to automatically migrate consumer codebases when major API changes are rolled out.
- Avoid sudden breaking changes. Mark old features as deprecated using JSDoc
4. What Candidates Miss
Many developers fail design system interviews by making the following mistakes:
- Building From Scratch Without Headless Libraries: Trying to manually implement the complete WAI-ARIA combobox or datepicker spec during an interview. Mentioning Radix UI or React Aria signals real-world production experience.
- Hardcoded Values: Designing a component library that lacks a token translation system. You must show how spacing and color changes update automatically.
- Forgetting Ref Forwarding & Spreading: If you write custom wrappers that block native HTML events or element measurements, developers will immediately bypass your components.
- No Governance Blueprint: Ignoring the organizational challenges. You must address how internal developers request updates (e.g., Request for Comments / RFC processes) and how you avoid creating duplicate, conflicting component wrappers.
5. Code Implementation & Deep Dives
Here are clean, concrete code snippets illustrating design tokens, primitive components, and composite compound architectures.
A. Spacing & Color Tokens (Style Dictionary Source)
This JSON structure represents the raw tokens file inside your design repository:
{
"color": {
"brand": {
"blue": { "500": { "value": "#3b82f6" } }
},
"semantic": {
"bg": {
"primary": { "value": "{color.brand.blue.500}" }
}
}
},
"spacing": {
"scale": {
"xs": { "value": "4px" },
"sm": { "value": "8px" },
"md": { "value": "16px" }
}
}
}This compiles into clean CSS Custom Properties on build:
:root {
--color-brand-blue-500: #3b82f6;
--color-semantic-bg-primary: var(--color-brand-blue-500);
--spacing-scale-xs: 4px;
--spacing-scale-sm: 8px;
--spacing-scale-md: 16px;
}B. The Primitive Layer: Ref-Forwarding Accessible Button
Here is a robust React component demonstrating ref forwarding, type safety, attribute spreading, and dynamic class configuration:
import * as React from "react";
// 1. Extend the native button attributes for seamless consumer integration
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
}
// 2. Wrap the component with React.forwardRef to allow parent measurements
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className = "",
variant = "primary",
size = "md",
isLoading = false,
disabled,
children,
...rest
},
ref,
) => {
// Standard style classes mapped to our CSS Token structure
const baseStyles =
"inline-flex items-center justify-center font-medium rounded transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50 disabled:pointer-events-none";
const variantStyles = {
primary:
"bg-[var(--color-semantic-bg-primary)] text-white hover:bg-opacity-90 focus-visible:outline-[var(--color-semantic-bg-primary)]",
secondary:
"bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus-visible:outline-gray-500",
danger:
"bg-red-600 text-white hover:bg-red-700 focus-visible:outline-red-600",
};
const sizeStyles = {
sm: "px-2.5 py-1.5 text-xs",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base",
};
const combinedClasses =
`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`.trim();
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={combinedClasses}
// Accessibility annotations
aria-busy={isLoading}
{...rest}
>
{isLoading ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Loading...
</>
) : (
children
)}
</button>
);
},
);
Button.displayName = "Button";C. The Composite Layer: Accessible Tabs (Compound Pattern)
This example shows how to design a complex interactive pattern using React Context. It includes custom keyboard controls (horizontal arrow navigation) to meet ARIA accessibility standards:
import * as React from "react";
// 1. Establish context for the parent container
interface TabsContextType {
activeValue: string;
setActiveValue: (val: string) => void;
}
const TabsContext = React.createContext<TabsContextType | undefined>(undefined);
function useTabs() {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error(
"Tabs compound components must be rendered inside a <Tabs /> container",
);
}
return context;
}
// 2. Main Container
export interface TabsProps {
defaultValue: string;
children: React.ReactNode;
}
export function Tabs({ defaultValue, children }: TabsProps) {
const [activeValue, setActiveValue] = React.useState(defaultValue);
return (
<TabsContext.Provider value={{ activeValue, setActiveValue }}>
<div className="flex flex-col w-full">{children}</div>
</TabsContext.Provider>
);
}
// 3. Tab List Wrapper (Holds the tab buttons)
export interface TabsListProps {
children: React.ReactNode;
"aria-label": string;
}
Tabs.List = function TabsList({
children,
"aria-label": label,
}: TabsListProps) {
const listRef = React.useRef<HTMLDivElement>(null);
// Implement arrow-key navigation for accessibility
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!listRef.current) return;
const focusable = Array.from(
listRef.current.querySelectorAll('[role="tab"]'),
) as HTMLElement[];
const index = focusable.indexOf(document.activeElement as HTMLElement);
if (index === -1) return;
let nextIndex = index;
if (e.key === "ArrowRight") {
nextIndex = (index + 1) % focusable.length;
} else if (e.key === "ArrowLeft") {
nextIndex = (index - 1 + focusable.length) % focusable.length;
}
if (nextIndex !== index) {
focusable[nextIndex].focus();
focusable[nextIndex].click(); // Trigger activation
}
};
return (
<div
ref={listRef}
role="tablist"
aria-label={label}
onKeyDown={handleKeyDown}
className="flex border-b border-gray-200 gap-2"
>
{children}
</div>
);
};
// 4. Tab Activator Trigger
export interface TabsTriggerProps {
value: string;
children: React.ReactNode;
}
Tabs.Trigger = function TabsTrigger({ value, children }: TabsTriggerProps) {
const { activeValue, setActiveValue } = useTabs();
const isSelected = activeValue === value;
return (
<button
role="tab"
aria-selected={isSelected}
tabIndex={isSelected ? 0 : -1}
aria-controls={`panel-${value}`}
id={`tab-${value}`}
onClick={() => setActiveValue(value)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
isSelected
? "border-[var(--color-semantic-bg-primary)] text-[var(--color-semantic-bg-primary)]"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
{children}
</button>
);
};
// 5. Tab Content Panel
export interface TabsContentProps {
value: string;
children: React.ReactNode;
}
Tabs.Content = function TabsContent({ value, children }: TabsContentProps) {
const { activeValue } = useTabs();
const isSelected = activeValue === value;
return (
<div
id={`panel-${value}`}
role="tabpanel"
aria-labelledby={`tab-${value}`}
tabIndex={0}
hidden={!isSelected}
className="py-4 focus:outline-none"
>
{isSelected && children}
</div>
);
};6. Summary & Key Takeaways
- Design Tokens First: Always start your answer with design tokens. Group them into Brand/Global, Semantic (Alias), and Component layers.
- Separate Primitives from Composites: Design primitive components (like
ButtonorBox) to be low-opinion and reusable, while styling-opinionated composite components are constructed from them. - API Quality: Provide ref forwarding, spread extra HTML props (
...rest), and implement compound component layouts to maximize developer productivity (DX). - Accessibility (a11y) is Essential: Utilize headless libraries to handle interaction states, implement clean focus traps for overlay templates, and support thorough keyboard layout navigations natively.
- Scale Adoption: Explain the lifecycle of updates—how components are versioned inside monorepos, how they are documented using Storybook, and how updates are distributed using automated code migrations (Codemods).
