A well-designed component library accelerates development and ensures consistency. Here's how to build one right.
Foundation Principles
Composability
Components should compose well together.
// Bad: Monolithic component
<ComplexCard
showImage={true}
showTitle={true}
showDescription={true}
showActions={true}
imagePosition="left"
/>
// Good: Composable components
<Card>
<CardImage src="..." />
<CardBody>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardActions>
<Button>Action</Button>
</CardActions>
</CardBody>
</Card>Consistency
Establish and follow patterns.
// Consistent prop naming
<Button variant="primary" size="lg" disabled={false} />
<Input variant="outlined" size="lg" disabled={false} />
<Select variant="outlined" size="lg" disabled={false} />Accessibility Built-In
Accessibility shouldn't be an afterthought.
// Accessible by default
function Button({ children, ...props }) {
return (
<button
type="button"
{...props}
className={cn(
'base-styles',
props.disabled && 'disabled-styles'
)}
>
{children}
</button>
);
}Component API Design
Props Design
Make intuitive, flexible APIs.
// Good: Clear, minimal props
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
onClick?: () => void;
}Compound Components
Related components that share state.
// Usage
<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>
Are you sure you want to proceed?
</DialogDescription>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogAction>Confirm</DialogAction>
</DialogFooter>
</DialogContent>
</Dialog>
// Implementation
const DialogContext = createContext({});
function Dialog({ children, ...props }) {
const [open, setOpen] = useState(false);
return (
<DialogContext.Provider value={{ open, setOpen }}>
{/* ... */}
</DialogContext.Provider>
);
}
function DialogTrigger({ children }) {
const { setOpen } = useContext(DialogContext);
return <button onClick={() => setOpen(true)}>{children}</button>;
}Render Props
Maximum flexibility with callbacks.
function DataFetcher({ url, render, renderLoading, renderError }) {
const { data, loading, error } = useFetch(url);
if (loading) return renderLoading?.();
if (error) return renderError?.(error);
return render(data);
}
// Usage
<DataFetcher
url="/api/users"
renderLoading={() => <Spinner />}
renderError={(error) => <Error message={error.message} />}
render={(users) => <UserList users={users} />}
/>Styling Strategy
Theme Integration
Components should use design tokens.
// Theme configuration
const theme = {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0ea5e9',
900: '#0c4a6e',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
};
// Component usage
function Button() {
return (
<button
style={{
padding: theme.spacing.md,
background: theme.colors.primary[500],
}}
>
Click me
</button>
);
}Style Overrides
Allow customization while providing defaults.
function Button({ className, style, ...props }) {
return (
<button
className={cn('btn-base', className)}
style={style}
{...props}
/>
);
}
// Usage with Tailwind
<Button className="bg-blue-500 hover:bg-blue-600" />
// Usage with custom styles
<Button style={{ background: 'custom-color' }} />Documentation
Storybook Integration
Visual documentation and testing.
// Button.stories.tsx
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
export const WithIcon = {
args: {
variant: 'primary',
leftIcon: <Icon name="star" />,
children: 'With Icon',
},
};Prop Table Documentation
## Button
Primary action button for the application.
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' | 'secondary' | 'ghost' | 'primary' | Visual style |
| size | 'sm' | 'md' | 'lg' | 'md' | Button size |
| disabled | boolean | false | Disable interaction |
| loading | boolean | false | Show loading state |
| children | ReactNode | - | Button content |
### Examples
<Button variant="primary" size="lg"> Submit </Button>
Testing Strategy
Visual Regression Testing
Catch unintended visual changes.
// Chromatic with Storybook
test('Button visual variants', async () => {
await expect(page).toHaveScreenshot('button-primary.png');
await expect(page).toHaveScreenshot('button-secondary.png');
});Accessibility Testing
Automated a11y checks.
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('Button should not have accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Versioning and Releases
Semantic Versioning
Follow SemVer for breaking changes.
- Major: Breaking changes
- Minor: New features, backward compatible
- Patch: Bug fixes
Changelog
Document all changes.
``markdown
[2.0.0] - 2024-03-01
Added
- Button loading state
- Card compound components
Changed
- Button API breaking changes
- Updated dependencies
Deprecated
- Old Button component (use ButtonV2)
Removed
- Legacy components
## Distribution
### Package Configuration
{ "name": "@your-org/ui-library", "version": "2.0.0", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "files": [ "dist" ], "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }
### Build Process
Multiple output formats.
// rollup.config.js export default { input: 'src/index.ts', output: [ { file: 'dist/index.js', format: 'cjs', }, { file: 'dist/index.esm.js', format: 'esm', }, { file: 'dist/index.ts', format: 'es', types: true, }, ], };
## Best Practices
### DOs
- Start with accessibility
- Provide sensible defaults
- Make APIs intuitive
- Document everything
- Test thoroughly
- Version carefully
### DON'Ts
- Over-abstract early
- Create rigid components
- Ignore performance
- Skip documentation
- Break changes without major version bump
- Assume one use case
## Performance
### Tree Shaking
Enable tree shaking for small bundles.
// esm/index.js export { Button } from './Button'; export { Input } from './Input'; export * from './utils';
// User can import what they need import { Button } from '@your-org/ui-library';
### Memoization
Optimize expensive renders.
export const Button = memo(function Button({ children, ...props }) { return ( <button {...props}> {children} </button> ); });
## Conclusion
Great component libraries are built on strong foundations: composability, consistency, accessibility, and excellent documentation. Start small, iterate based on feedback, and maintain backward compatibility. Your library should make developers more productive, not frustrated.