Building a Modern Component Library
Building a component library from scratch can seem daunting, but with the right approach and tools, you can create a robust, maintainable system that scales with your team.
Setting Up TypeScript
First, let’s set up a proper TypeScript configuration optimized for component development:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Creating Reusable Components
Here’s an example of a well-typed Button component with variants:
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
isLoading = false,
disabled,
...props
}) => {
const baseClasses = 'rounded-lg font-semibold transition-all';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? 'Loading...' : children}
</button>
);
};
Design Tokens with CSS Variables
Implementing a token system ensures consistency across your components:
:root {
/* Colors */
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-gray-100: #f3f4f6;
--color-gray-900: #111827;
/* Spacing */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-4: 1rem;
--spacing-8: 2rem;
/* Typography */
--font-sans: 'Inter', system-ui, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
}
Testing Components
Every component should have comprehensive tests. Here’s an example using React Testing Library:
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders children correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button isLoading>Click me</Button>);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('is disabled when loading', () => {
render(<Button isLoading>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Building and Publishing
Set up your build process with modern tooling:
{
"name": "@yourorg/components",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"test": "jest",
"storybook": "storybook dev -p 6006"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Conclusion
A well-architected component library accelerates development and ensures consistency. Focus on:
- Type safety with TypeScript
- Composability with flexible props
- Consistency through design tokens
- Quality with comprehensive testing
With these foundations, your team can build features faster while maintaining high standards.