Component Library Creation Guide
Status: Complete Implementation Guide
Version: 1.0
Purpose: Step-by-step procedures for building scalable, reusable component libraries
Applicable To: Any web application development project
Overview
This guide provides comprehensive procedures for creating a production-ready component library that ensures consistency, accessibility, and developer efficiency. The approach follows atomic design principles and emphasizes reusability, maintainability, and scalability.
Key Benefits
- Consistency: Unified design language across entire application
- Speed: Rapid development with pre-built components
- Quality: Built-in accessibility and best practices
- Scalability: Composable components grow with your needs
Component Architecture Setup
Step 1: Atomic Design Structure
Organize components following atomic design methodology:
src/components/
βββ atoms/ # Basic building blocks
β βββ Button/
β βββ Input/
β βββ Icon/
β βββ Typography/
βββ molecules/ # Simple combinations
β βββ FormField/
β βββ Card/
β βββ Alert/
β βββ SearchBox/
βββ organisms/ # Complex combinations
β βββ Header/
β βββ Form/
β βββ DataTable/
β βββ Navigation/
βββ templates/ # Page layouts
βββ DashboardLayout/
βββ FormLayout/
βββ ContentLayout/
Step 2: Component Foundation
Create base component structure with TypeScript:
// src/components/types.ts - Component type definitions
export interface BaseComponentProps {
className?: string;
children?: React.ReactNode;
'data-testid'?: string;
}
export interface ThemeProps {
variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'success';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
}
export interface InteractiveProps {
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
}
// Component composition utilities
export type ComponentVariants<T> = {
[K in keyof T]: T[K];
};
export type ComponentSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export type ComponentVariant = 'primary' | 'secondary' | 'tertiary' | 'danger' | 'success';
Step 3: Design Token Integration
// src/components/design-tokens.ts - Design system tokens
export const designTokens = {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a8a'
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
300: '#d1d5db',
500: '#6b7280',
700: '#374151',
900: '#111827'
},
semantic: {
success: '#059669',
warning: '#d97706',
danger: '#dc2626',
info: '#0891b2'
}
},
spacing: {
xs: '0.25rem', // 4px
sm: '0.5rem', // 8px
md: '1rem', // 16px
lg: '1.5rem', // 24px
xl: '2rem', // 32px
'2xl': '3rem' // 48px
},
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Monaco', 'Consolas', 'monospace']
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }]
}
},
borderRadius: {
none: '0',
sm: '0.125rem',
DEFAULT: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem'
}
};
π Atomic Components Implementation
Step 1: Button Component
// src/components/atoms/Button/Button.tsx
import React from 'react';
import { BaseComponentProps, ThemeProps, InteractiveProps } from '../../types';
import './Button.css';
export interface ButtonProps extends
BaseComponentProps,
ThemeProps,
InteractiveProps {
type?: 'button' | 'submit' | 'reset';
fullWidth?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
}
export const Button: React.FC<ButtonProps> = ({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
fullWidth = false,
icon,
iconPosition = 'left',
type = 'button',
onClick,
'data-testid': testId,
...props
}) => {
const classes = [
'btn',
`btn-${variant}`,
`btn-${size}`,
fullWidth && 'btn-full-width',
loading && 'btn-loading',
disabled && 'btn-disabled',
className
].filter(Boolean).join(' ');
const handleClick = () => {
if (!disabled && !loading && onClick) {
onClick();
}
};
return (
<button
type={type}
className={classes}
disabled={disabled || loading}
onClick={handleClick}
data-testid={testId}
aria-disabled={disabled || loading}
{...props}
>
{loading && <span className="btn-spinner" aria-hidden="true" />}
{icon && iconPosition === 'left' && (
<span className="btn-icon btn-icon-left" aria-hidden="true">
{icon}
</span>
)}
<span className="btn-content">{children}</span>
{icon && iconPosition === 'right' && (
<span className="btn-icon btn-icon-right" aria-hidden="true">
{icon}
</span>
)}
</button>
);
};
Step 2: Button Styles
/* src/components/atoms/Button/Button.css */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: inherit;
font-weight: 500;
text-decoration: none;
border: none;
border-radius: 0.375rem;
cursor: pointer;
transition: all 150ms ease;
position: relative;
white-space: nowrap;
user-select: none;
}
.btn:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.btn:focus-visible {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
/* Size variants */
.btn-xs {
height: 1.75rem;
padding: 0 0.5rem;
font-size: 0.75rem;
line-height: 1rem;
}
.btn-sm {
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.btn-md {
height: 2.5rem;
padding: 0 1rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.btn-lg {
height: 3rem;
padding: 0 1.5rem;
font-size: 1rem;
line-height: 1.5rem;
}
.btn-xl {
height: 3.5rem;
padding: 0 2rem;
font-size: 1.125rem;
line-height: 1.75rem;
}
/* Variant styles */
.btn-primary {
background-color: var(--color-primary-600);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-primary-700);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(37, 99, 235, 0.25);
}
.btn-secondary {
background-color: white;
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--color-gray-50);
border-color: var(--color-gray-400);
}
.btn-danger {
background-color: var(--color-danger);
color: white;
}
.btn-danger:hover:not(:disabled) {
background-color: var(--color-red-700);
}
/* State styles */
.btn:disabled,
.btn-disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
.btn-loading {
color: transparent;
}
.btn-loading .btn-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1rem;
height: 1rem;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.btn-full-width {
width: 100%;
}
/* Icon positioning */
.btn-icon {
display: flex;
align-items: center;
}
.btn-icon-left {
margin-right: -0.25rem;
}
.btn-icon-right {
margin-left: -0.25rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Step 3: Input Component
// src/components/atoms/Input/Input.tsx
import React, { forwardRef } from 'react';
import { BaseComponentProps } from '../../types';
import './Input.css';
export interface InputProps extends
BaseComponentProps,
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: 'sm' | 'md' | 'lg';
state?: 'default' | 'error' | 'success';
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(({
className = '',
size = 'md',
state = 'default',
leftIcon,
rightIcon,
disabled,
'data-testid': testId,
...props
}, ref) => {
const classes = [
'input',
`input-${size}`,
`input-${state}`,
leftIcon && 'input-has-left-icon',
rightIcon && 'input-has-right-icon',
disabled && 'input-disabled',
className
].filter(Boolean).join(' ');
return (
<div className="input-wrapper">
{leftIcon && (
<span className="input-icon input-icon-left" aria-hidden="true">
{leftIcon}
</span>
)}
<input
ref={ref}
className={classes}
disabled={disabled}
data-testid={testId}
{...props}
/>
{rightIcon && (
<span className="input-icon input-icon-right" aria-hidden="true">
{rightIcon}
</span>
)}
</div>
);
});
Input.displayName = 'Input';
𧬠Molecular Components Implementation
Step 1: Form Field Component
// src/components/molecules/FormField/FormField.tsx
import React from 'react';
import { BaseComponentProps } from '../../types';
import './FormField.css';
export interface FormFieldProps extends BaseComponentProps {
label?: string;
hint?: string;
error?: string;
required?: boolean;
children: React.ReactElement;
}
export const FormField: React.FC<FormFieldProps> = ({
label,
hint,
error,
required = false,
children,
className = '',
'data-testid': testId
}) => {
const fieldId = children.props.id || `field-${Math.random().toString(36).substr(2, 9)}`;
const hintId = hint ? `${fieldId}-hint` : undefined;
const errorId = error ? `${fieldId}-error` : undefined;
// Clone child element with proper ARIA attributes
const childWithProps = React.cloneElement(children, {
id: fieldId,
'aria-describedby': [hintId, errorId].filter(Boolean).join(' ') || undefined,
'aria-invalid': error ? 'true' : undefined,
state: error ? 'error' : children.props.state
});
const classes = [
'form-field',
error && 'form-field-error',
className
].filter(Boolean).join(' ');
return (
<div className={classes} data-testid={testId}>
{label && (
<label htmlFor={fieldId} className="form-label">
{label}
{required && (
<span className="form-label-required" aria-label="required">
*
</span>
)}
</label>
)}
{childWithProps}
{hint && !error && (
<p id={hintId} className="form-hint">
{hint}
</p>
)}
{error && (
<p id={errorId} className="form-error" role="alert">
{error}
</p>
)}
</div>
);
};
Step 2: Card Component
// src/components/molecules/Card/Card.tsx
import React from 'react';
import { BaseComponentProps } from '../../types';
import './Card.css';
export interface CardProps extends BaseComponentProps {
variant?: 'default' | 'interactive' | 'outlined';
padding?: 'none' | 'sm' | 'md' | 'lg';
shadow?: 'none' | 'sm' | 'md' | 'lg';
}
export const Card: React.FC<CardProps> = ({
children,
className = '',
variant = 'default',
padding = 'md',
shadow = 'sm',
'data-testid': testId,
...props
}) => {
const classes = [
'card',
`card-${variant}`,
`card-padding-${padding}`,
`card-shadow-${shadow}`,
className
].filter(Boolean).join(' ');
return (
<div
className={classes}
data-testid={testId}
{...props}
>
{children}
</div>
);
};
// Card sub-components
export const CardHeader: React.FC<BaseComponentProps> = ({
children,
className = '',
...props
}) => (
<div className={`card-header ${className}`} {...props}>
{children}
</div>
);
export const CardBody: React.FC<BaseComponentProps> = ({
children,
className = '',
...props
}) => (
<div className={`card-body ${className}`} {...props}>
{children}
</div>
);
export const CardFooter: React.FC<BaseComponentProps> = ({
children,
className = '',
...props
}) => (
<div className={`card-footer ${className}`} {...props}>
{children}
</div>
);
Component Development Workflow
Step 1: Storybook Setup
// .storybook/main.ts - Storybook configuration
module.exports = {
stories: ['../src/components/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-design-tokens'
],
framework: '@storybook/react'
};
// src/components/atoms/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'tertiary', 'danger', 'success'],
},
size: {
control: { type: 'select' },
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Button',
},
};
export const WithIcon: Story = {
args: {
variant: 'primary',
children: 'Send Email',
icon: <span>π§</span>,
},
};
export const Loading: Story = {
args: {
variant: 'primary',
children: 'Sending...',
loading: true,
},
};
export const Disabled: Story = {
args: {
variant: 'primary',
children: 'Disabled',
disabled: true,
},
};
Step 2: Component Testing
// src/components/atoms/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick} disabled>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
it('shows loading state correctly', () => {
render(<Button loading>Loading</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-loading');
expect(button).toBeDisabled();
});
it('applies correct variant classes', () => {
render(<Button variant="secondary">Button</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-secondary');
});
it('supports custom test id', () => {
render(<Button data-testid="custom-button">Button</Button>);
expect(screen.getByTestId('custom-button')).toBeInTheDocument();
});
});
Step 3: Documentation Generation
// scripts/generate-docs.ts - Auto-generate component documentation
import fs from 'fs';
import path from 'path';
import { Project } from 'ts-morph';
const project = new Project({
tsConfigFilePath: 'tsconfig.json',
});
const generateComponentDocs = () => {
const componentsDir = path.join(__dirname, '../src/components');
const outputDir = path.join(__dirname, '../docs/components');
// Ensure output directory exists
fs.mkdirSync(outputDir, { recursive: true });
// Find all component files
const componentFiles = project.getSourceFiles()
.filter(file => file.getFilePath().includes('/components/'))
.filter(file => file.getBaseName().endsWith('.tsx'));
componentFiles.forEach(file => {
const interfaces = file.getInterfaces();
const components = file.getFunctions().concat(file.getVariableDeclarations());
const docs = {
name: file.getBaseName().replace('.tsx', ''),
path: file.getFilePath(),
interfaces: interfaces.map(iface => ({
name: iface.getName(),
properties: iface.getProperties().map(prop => ({
name: prop.getName(),
type: prop.getType().getText(),
optional: prop.hasQuestionToken(),
docs: prop.getJsDocs().map(doc => doc.getDescription()).join('\n')
}))
})),
examples: [] // Could be extracted from stories
};
const outputPath = path.join(outputDir, `${docs.name}.json`);
fs.writeFileSync(outputPath, JSON.stringify(docs, null, 2));
});
};
generateComponentDocs();
Component Library Implementation Checklist
Foundation Setup
- Atomic design directory structure
- TypeScript interfaces and types
- Design token integration
- Base component utilities
- CSS architecture and naming conventions
Atomic Components
- Button with all variants and states
- Input with validation states
- Typography components
- Icon system
- Basic form controls (checkbox, radio, select)
Molecular Components
- Form field with label and validation
- Card with header, body, footer
- Alert/notification component
- Search box component
- Navigation items
Organism Components
- Complete forms
- Data tables
- Navigation bars
- Complex UI patterns
- Layout components
Documentation & Testing
- Storybook setup with all components
- Comprehensive unit tests
- Accessibility testing
- Visual regression testing
- Auto-generated documentation
Quality Assurance
- Accessibility compliance (WCAG 2.1 AA)
- Cross-browser compatibility
- Mobile responsiveness
- Performance optimization
- Bundle size analysis
This guide provides a comprehensive framework for building a production-ready component library. Regular updates, community feedback, and usage analytics will help evolve the library to meet changing needs.