Skip to content
Design

Building Component Libraries

Architecture, tooling, and patterns for building reusable React component libraries.

stellae.design

11 min read

Introduction

A component library is the implementation layer of your design system — reusable React components that enforce consistency. Building one well means your team ships faster with fewer visual bugs.

But a component library requires thoughtful API design, built-in accessibility, documentation, and a publishing pipeline. The architecture decisions you make early determine whether it scales.

This guide covers project setup, component patterns, build tooling, and publishing as an npm package.


Key Concepts

Component API Design with CVA

typescript
import { forwardRef, type ComponentPropsWithRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
        ghost: 'hover:bg-gray-100',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-6 text-base' },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

interface ButtonProps extends ComponentPropsWithRef<'button'>, VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, size, isLoading, children, className, ...props }, ref) => (
    <button ref={ref} className={buttonVariants({ variant, size, className })}
      disabled={isLoading || props.disabled} {...props}>
      {isLoading && <Spinner className="mr-2" />}
      {children}
    </button>
  )
);

Build Configuration

typescript
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  clean: true,
  external: ['react', 'react-dom'],
  sourcemap: true,
  treeshake: true,
});

Practical Examples

1. Compound Component Pattern

typescript
const CardContext = createContext<{ variant: string }>({ variant: 'default' });

function Card({ variant = 'default', children, className }: CardProps) {
  return (
    <CardContext.Provider value={{ variant }}>
      <div className={cn('rounded-lg border bg-white p-6', className)}>{children}</div>
    </CardContext.Provider>
  );
}
Card.Header = ({ children }) => <div className="mb-4">{children}</div>;
Card.Title = ({ children }) => <h3 className="text-lg font-semibold">{children}</h3>;

// Usage
<Card>
  <Card.Header><Card.Title>Dashboard</Card.Title></Card.Header>
  <p>Content</p>
</Card>

2. Polymorphic Component

typescript
type AsProp<C extends React.ElementType> = { as?: C };

export function Text<C extends React.ElementType = 'p'>({
  as, children, className, ...props
}: AsProp<C> & Omit<React.ComponentPropsWithRef<C>, 'as'> & { className?: string }) {
  const Component = as || 'p';
  return <Component className={className} {...props}>{children}</Component>;
}

<Text>Paragraph</Text>
<Text as="h1" className="text-4xl font-bold">Heading</Text>
<Text as="span">Inline</Text>

3. Project Structure

plain
my-ui/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.test.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   └── index.ts
│   │   ├── Input/
│   │   └── Dialog/
│   ├── tokens/
│   └── index.ts          # Public API
├── tsup.config.ts
└── package.json

Best Practices

  • ✅ Extend native HTML element props — don't rebuild what the browser gives you
  • ✅ Use forwardRef for all components — consumers need ref access
  • ✅ Use CVA for type-safe variant management
  • ✅ Export types alongside components for TypeScript consumers
  • ✅ Keep the API surface small — fewer props, more composition
  • ❌ Don't bundle React — mark it as a peer dependency
  • ❌ Don't couple to a specific styling solution — support className overrides

Common Pitfalls

  • Making components too opinionated — hard to customize for edge cases
  • Not testing with screen readers — accessibility bugs sneak in
  • Barrel exports causing bundle issues — use tree-shakeable ESM
  • Breaking changes without semver — consumers lose trust

Designdesign

Was this article helpful?