New: We've launched a brand new Coding Challenges section! Check out these interactive, real-world exercises to level up your skills.Explore Challenges
FrontendPrep
system-designHard

Design a Component Library / Design System

Loading...

Master the frontend system design of a component library. Learn about design tokens, primitive vs composite components, API design, accessibility, theming, and versioning strategies.

Arvind M
Arvind MLinkedIn

Frontend System Design Master Guide

This question is part of our comprehensive Frontend System Design Series. Master the complete framework, requirements, and edge cases before diving in.

Read Guide

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. The 7-Step Answer Framework

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:

  1. Global / Tier 1 Tokens (System/Brand): Abstract, raw values that represent the brand's complete palette.
    • Example: color-blue-500: #3B82F6, spacing-4: 16px.
  2. 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) or color-bg-primary: color-gray-900 (in dark mode).
  3. 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.

3-Tier Design Token Hierarchy

Token Pipeline Architecture

Use Style Dictionary (or a similar build-time tool) to transform raw JSON token files into platform-specific outputs:

Cross-Platform Token Pipeline


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.forwardRef to 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", or disabled) seamlessly via ...rest props.
    • Ensure Type Extensibility: Extend native HTML element types (e.g., React.ButtonHTMLAttributes<HTMLButtonElement>).

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), Card layouts.

  • 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.

  1. 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.
  2. 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.
  3. Keyboard Interactivity:
    • Buttons react to Space and Enter.
    • Menus, Select dropdowns, and Tabs navigate using Arrow Keys.
    • Modals and dropdowns dismiss on Escape.
  4. 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 @deprecated warnings 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.

4. What Candidates Miss

Many developers fail design system interviews by making the following mistakes:

  1. 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.
  2. Hardcoded Values: Designing a component library that lacks a token translation system. You must show how spacing and color changes update automatically.
  3. Forgetting Ref Forwarding & Spreading: If you write custom wrappers that block native HTML events or element measurements, developers will immediately bypass your components.
  4. 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 Button or Box) 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).

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.