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
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
// 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
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
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
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
Related Guides
- Design Tokens Complete Guide — The foundation your components consume
- Storybook Setup Guide — Document and test components visually
- Design System Versioning — Manage releases and breaking changes
- Design System Documentation — Make your library easy to adopt