Building a component library with React and TypeScript is a strategic investment that pays dividends across projects, teams, and organizations. A well-designed component library establishes visual consistency, accelerates development workflows, and creates a shared language between designers and developers. In 2025, the landscape of component library development has evolved significantly, with modern tooling, enhanced type safety, and AI-assisted development transforming how we approach reusable UI systems.
This guide walks you through creating a production-ready React component library with TypeScript, covering everything from initial setup to publishing and maintaining your library over time. Whether you're building a design system for a large organization or creating reusable components for multiple projects, mastering component library development is essential for professional web development.
Key benefits of investing in a custom component library
Visual Consistency
A centralized library ensures all your digital products maintain coherent design language and user experience.
Development Velocity
Developers leverage tested, documented components instead of reinventing common UI patterns.
Type Safety
TypeScript provides autocomplete and compile-time checking that prevents integration bugs.
Single Source of Truth
Updates propagate through all projects automatically, eliminating duplication and drift.
Setting Up Your Project
The foundation of a successful component library begins with thoughtful project initialization. Selecting the right bundler is crucial--modern options like Vite, Rollup, and tsup each offer distinct advantages.
Choosing Your Bundler
Vite provides an excellent developer experience with its fast development server and streamlined build configuration. Rollup has long been the gold standard for library bundling, offering fine-grained control over output formats and tree-shaking optimization. Tsup, built on top of Rollup and written in TypeScript, simplifies the configuration process while maintaining powerful capabilities. For most React component libraries in 2025, tsup represents an excellent balance of simplicity and power.
Package Configuration
Your package.json configuration requires careful attention to ensure compatibility across different environments:
{
"name": "your-component-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"peerDependencies": {
"react": ">=18"
},
"sideEffects": false
}
The peerDependencies field should list React and potentially other peer dependencies that consumers of your library will need to have installed. The sideEffects field, when set to false, enables tree-shaking by telling bundlers that your package is free of side effects.
TypeScript Configuration
The TypeScript configuration for a component library differs significantly from application TypeScript configs. You need to generate declaration files (.d.ts) that TypeScript consumers of your library will use for type checking.
TypeScript Best Practices for Components
Writing TypeScript types for React components requires balancing flexibility with safety. Generic props allow components to adapt to different data types while maintaining type safety.
Generic Components
For components that work with various data structures, generics provide type safety while maintaining flexibility:
interface DataTableProps<T extends Record<string, unknown>> {
data: T[];
columns: Array<{
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}>;
onRowClick?: (row: T) => void;
}
function DataTable<T extends Record<string, unknown>>({
data,
columns,
onRowClick
}: DataTableProps<T>) {
// Component implementation
}
Union Types and Literal Types
Union types and literal types provide excellent tools for creating constrained, self-documenting props. Instead of accepting any string for a button variant prop, use a union of literal values that enumerate the allowed options.
type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
children: React.ReactNode;
}
This approach enables TypeScript to provide autocomplete suggestions and catch typos at compile time.
The 'as const' Assertion
The as const assertion becomes invaluable when defining default props or configuration objects. By asserting that an object is readonly and that its property values are literal types, you enable TypeScript to infer the narrowest possible types.
Organizing Component Structure
A well-organized component library structure supports maintainability and discoverability. Grouping related components into directories creates logical boundaries that help developers locate the right component.
Recommended Directory Structure
src/
├── Button/
│ ├── Button.tsx
│ ├── Button.types.ts
│ ├── Button.test.tsx
│ ├── Button.stories.tsx
│ └── index.ts
├── Input/
│ ├── Input.tsx
│ ├── Input.types.ts
│ ├── Input.test.tsx
│ ├── Input.stories.tsx
│ └── index.ts
├── hooks/
│ ├── useClipboard/
│ │ ├── useClipboard.ts
│ │ ├── useClipboard.test.ts
│ │ └── index.ts
│ └── index.ts
├── utils/
│ └── index.ts
├── index.ts
└── package.json
Each component deserves its own directory containing the component implementation, types, tests, and stories. This co-location keeps related files together and simplifies refactoring.
The Barrel Pattern
The root index.ts file aggregates exports from all components, providing a single import path for consumers. Using the barrel pattern at the root level simplifies imports while still allowing direct imports from specific component files for tree-shaking benefits.
Building and Bundling Your Library
Modern bundlers offer sophisticated capabilities for creating optimized library bundles. The build configuration should produce multiple output formats.
Output Formats
- ES Modules (.mjs): Modern bundlers support ESM natively, enabling tree-shaking
- CommonJS (.js): Node.js and older bundlers require CommonJS format
- UMD: Fallback for direct browser usage via script tags
Tsup Configuration
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs', 'iife'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
external: ['react', 'react-dom'],
banner: {
js: `/**
* @license MIT
* Copyright (c) 2025 Your Organization
*/`,
},
});
Tree-Shaking
Tree-shaking represents one of the most significant benefits of modern bundling for component libraries. By properly configuring your package.json sideEffects field, bundlers can eliminate unused code from the final application bundle.
Source Maps
Source maps enable developers debugging applications that use your library to step into your component source code rather than viewing compiled, minified output.
Testing Your Components
Comprehensive testing protects the investment you've made in your component library and gives consumers confidence in using your components.
Unit Testing with React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with default props', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies variant classes correctly', () => {
render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('button--primary');
});
});
Visual Regression Testing
Visual regression testing captures screenshots of your components and compares them against previous baselines. Tools like Chromatic or Storybook's visual testing addon automatically detect visual changes.
Key Testing Principles
- Test behavior, not implementation
- Use accessible queries (getByRole, getByLabelText)
- Test edge cases and error states
- Maintain high coverage for core components
Documenting with Storybook
Storybook serves as both a development environment and documentation hub for your component library.
Story Configuration
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'destructive', 'ghost'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
Autodocs
Autodocs automatically generates documentation pages from your component's TypeScript types and Storybook configurations. The generated documentation includes prop tables showing types, defaults, and descriptions, as well as examples pulled from your stories.
Story Organization
- Group components by category (Actions, Layout, Data Display)
- Include "knobs" for exploring prop combinations
- Create stories for all component variants
- Add documentation comments to props
Accessibility Considerations
Accessibility should be a first-class concern for any component library, not an afterthought. Building accessible components requires careful attention to how users with disabilities will interact with your interface. For organizations implementing AI automation, accessible component libraries form the foundation of inclusive digital products that serve all users effectively.
Core Requirements
- Keyboard Navigation: Every interactive element must be keyboard accessible
- Focus Management: Visible focus indicators and logical tab order
- Screen Reader Support: Appropriate ARIA attributes for assistive technologies
- Color Contrast: Meet WCAG contrast requirements
Testing Tools
- jest-axe: Integrates accessibility audits into unit tests
- Storybook Accessibility Addon: Real-time accessibility checking during development
- axe-core: Automated accessibility testing in browsers
Best Practices
- Use semantic HTML elements (button, input, nav)
- Provide visible focus indicators
- Support keyboard-only navigation
- Include ARIA labels for icon-only buttons
- Test with actual screen readers regularly
Publishing and Versioning
NPM publishing makes your component library available to consumers, whether that's other teams within your organization or external developers.
Package Metadata
Before publishing, ensure your package.json includes accurate metadata:
{
"name": "your-component-library",
"description": "A production-ready React component library",
"version": "1.0.0",
"license": "MIT",
"repository": "https://github.com/your-org/component-library",
"keywords": ["react", "components", "typescript", "ui"]
}
Semantic Versioning
- Patch (1.0.0 → 1.0.1): Bug fixes, no API changes
- Minor (1.0.0 → 1.1.0): New features, backward compatible
- Major (1.0.0 → 2.0.0): Breaking changes
CI/CD Automation
Continuous integration pipelines should:
- Run all tests on every commit
- Build and type-check on every PR
- Publish beta versions from feature branches
- Publish stable versions from tagged commits
- Generate and publish documentation sites
Performance Optimization
Component library performance affects every application that uses your library. Optimizing your web development practices by focusing on performance from the start prevents technical debt and ensures your components scale gracefully.
Bundle Size Optimization
- Enable tree-shaking with proper sideEffects configuration
- Use code splitting for heavy components
- Minimize dependencies
- Consider lazy loading for complex components
React Performance
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./Chart/Chart'));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
);
}
Optimization Techniques
- memo: Prevent unnecessary re-renders of pure components
- useMemo: Cache expensive calculations
- useCallback: Preserve function identity for child props
- Dynamic Imports: Load heavy components on demand
Best Practices Summary
Building a React component library with TypeScript requires attention to multiple concerns: type safety, build configuration, testing, documentation, and accessibility.
Key Takeaways
- Start with Structure: Organize components logically with co-located tests and stories
- Leverage TypeScript: Use generics, union types, and proper type exports
- Test Comprehensively: Unit tests, integration tests, and visual regression testing
- Document Thoroughly: Storybook provides development environment and documentation
- Prioritize Accessibility: Make accessibility a core requirement, not an afterthought
- Optimize Performance: Enable tree-shaking and use React's performance features
Recommended Tooling Stack (2025)
| Category | Recommendation |
|---|---|
| Bundler | tsup |
| Testing | React Testing Library + Vitest |
| Documentation | Storybook with autodocs |
| Visual Testing | Chromatic |
| Linting | ESLint + TypeScript |
| Formatting | Prettier |
The investment in building a component library pays dividends through consistent, well-tested, documented components that accelerate development across your organization.