Building a Production-Ready React Component Library: Step-by-Step Tutorial
Learn how to build a comprehensive React component library from scratch, following the exact approach I used to create React UI Kit. This complete tutorial covers 11 essential steps, from initial setup to deploying your components with visual regression testing and automated publishing.
๐ฏ What We'll Build
By the end of this tutorial, you'll have created:
Feature | Description |
---|---|
Monorepo Structure | 30+ components organized in packages |
TypeScript-First | Excellent IntelliSense and type safety |
Accessibility | WCAG 2.1 AA compliance built-in |
Visual Testing | Automated regression testing with Chromatic |
Documentation | Interactive Storybook with examples |
Production Ready | Automated publishing and CI/CD |
๐ Prerequisites
Before we start, ensure you have:
Requirement | Version | Purpose |
---|---|---|
Node.js | 22+ | JavaScript runtime |
pnpm | 10+ | Package manager (recommended) |
Git | Latest | Version control |
Knowledge | Basic React & TypeScript | Development foundation |
๐ Step 1: Project Setup and Monorepo Structure
Let's start by creating our monorepo structure using pnpm workspaces:
# Create the project directory
mkdir react-ui-kit
cd react-ui-kit
# Initialize package.json
npm init -y
# Install pnpm globally if you haven't
npm install -g pnpm
# Create the monorepo structure
mkdir -p packages/components
mkdir -p packages/design-tokens
mkdir -p packages/icons
mkdir -p packages/utils
mkdir apps
mkdir docs
๐ฆ Configure the Root package.json
{
"name": "react-ui-kit",
"private": true,
"version": "0.1.0",
"packageManager": "pnpm@10.12.1",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@react-ui-kit/components",
"lint": "turbo run lint",
"test": "turbo run test",
"storybook": "cd packages/components && pnpm run storybook",
"build-storybook": "cd packages/components && pnpm run build-storybook",
"chromatic": "cd packages/components && pnpm run chromatic"
},
"devDependencies": {
"@changesets/cli": "^2.29.5",
"turbo": "^2.5.4",
"husky": "^9.1.7",
"lint-staged": "^16.1.2"
}
}
๐ Create pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'docs/*'
โก Step 2: Setup Build Tool (Turbo)
Install and configure Turbo for monorepo build orchestration:
pnpm add -D turbo
Create turbo.json
:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {
"outputs": []
},
"test": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
๐ฆ Step 3: Create the Core Components Package
Navigate to the components package and set it up:
cd packages/components
pnpm init
โ๏ธ Configure Components package.json
{
"name": "@react-ui-kit/components",
"version": "0.1.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles.css": "./dist/styles.css"
},
"files": ["dist"],
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/**/*.{ts,tsx}",
"test": "vitest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"chromatic": "chromatic --project-token $CHROMATIC_PROJECT_TOKEN"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-button": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-slot": "^1.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.3.7",
"@storybook/addon-interactions": "^8.3.7",
"@storybook/addon-links": "^8.3.7",
"@storybook/blocks": "^8.3.7",
"@storybook/react": "^8.3.7",
"@storybook/react-vite": "^8.3.7",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"autoprefixer": "^10.4.20",
"chromatic": "^11.19.2",
"postcss": "^8.4.49",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"storybook": "^8.3.7",
"tailwindcss": "^3.4.17",
"tsup": "^8.3.5",
"typescript": "^5.8.3",
"vitest": "^2.1.4"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
}
๐ง Step 4: TypeScript Configuration
Create tsconfig.json
in the components package:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
๐จ Step 5: Tailwind CSS Setup
Create tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./stories/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}
๐จ Create Base Styles
Create src/styles/globals.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
๐งฉ Step 6: Create Your First Component - Button
Let's create a robust Button component using CVA (Class Variance Authority):
๐ Component Structure
mkdir -p src/components/ui
touch src/components/ui/button.tsx
touch src/lib/utils.ts
touch src/index.ts
๐ ๏ธ Utility Functions
Create src/lib/utils.ts
:
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
๐ Button Component
Create src/components/ui/button.tsx
:
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
๐ Export Components
Update src/index.ts
:
export { Button, type ButtonProps } from "./components/ui/button"
export { cn } from "./lib/utils"
๐ Step 7: Setup Storybook for Documentation
Initialize Storybook in your components package:
npx storybook@latest init
โ๏ธ Configure Storybook
Create .storybook/main.ts
:
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
typescript: {
reactDocgen: 'react-docgen-typescript',
},
};
export default config;
๐จ Configure Storybook Styles
Create .storybook/preview.ts
:
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
๐ Create Button Stories
Create src/components/ui/button.stories.tsx
:
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: { type: 'select' },
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
},
size: {
control: { type: 'select' },
options: ['default', 'sm', 'lg', 'icon'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary',
},
};
export const Destructive: Story = {
args: {
variant: 'destructive',
children: 'Destructive',
},
};
export const Outline: Story = {
args: {
variant: 'outline',
children: 'Outline',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
children: 'Ghost',
},
};
export const Link: Story = {
args: {
variant: 'link',
children: 'Link',
},
};
export const Small: Story = {
args: {
size: 'sm',
children: 'Small',
},
};
export const Large: Story = {
args: {
size: 'lg',
children: 'Large',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled',
},
};
๐งช Step 8: Testing Setup with Vitest
Create comprehensive tests for your components:
โ๏ธ Configure Vitest
Create vitest.config.ts
:
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});
๐ ๏ธ Test Setup
Create src/test/setup.ts
:
import '@testing-library/jest-dom';
๐งช Button Component Tests
Create src/components/ui/button.test.tsx
:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './button';
describe('Button', () => {
it('renders with default props', () => {
render(<Button>Test Button</Button>);
const button = screen.getByRole('button', { name: /test button/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('bg-primary', 'text-primary-foreground');
});
it('applies variant classes correctly', () => {
render(<Button variant="secondary">Secondary Button</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('bg-secondary', 'text-secondary-foreground');
});
it('applies size classes correctly', () => {
render(<Button size="lg">Large Button</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('h-11', 'px-8');
});
it('handles click events', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Clickable Button</Button>);
const button = screen.getByRole('button');
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled Button</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveClass('disabled:opacity-50', 'disabled:pointer-events-none');
});
it('renders as child component when asChild is true', () => {
render(
<Button asChild>
<a href="/test">Link Button</a>
</Button>
);
const link = screen.getByRole('link');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/test');
});
});
๐๏ธ Step 9: Visual Testing with Chromatic
Set up visual regression testing to catch UI changes:
๐ Setup Chromatic
- Create account at chromatic.com
- Connect your GitHub repository
- Get your project token
- Add token to your CI environment variables
โ๏ธ Configure GitHub Actions
Create .github/workflows/chromatic.yml
:
name: 'Chromatic'
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build components
run: pnpm run build --filter=@react-ui-kit/components
- name: Publish to Chromatic
uses: chromaui/action@v11
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: packages/components
buildScriptName: build-storybook
๐ Step 10: Publishing Setup with Changesets
Automate versioning and publishing:
๐ฆ Initialize Changesets
npx @changesets/cli init
โ๏ธ Configure Changesets
Edit .changeset/config.json
:
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
๐ CI/CD for Publishing
Create .github/workflows/release.yml
:
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: npm install -g pnpm
- name: Install Dependencies
run: pnpm install
- name: Build packages
run: pnpm run build
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: pnpm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
๐ Add Release Script
Add to root package.json
:
"scripts": {
"changeset": "changeset",
"version-packages": "changeset version",
"release": "changeset publish"
}
๐ก Step 11: Usage Example
Here's how to use your component library:
๐ฆ Installation
npm install @your-org/react-ui-kit
๐ฏ Implementation
import { Button } from "@your-org/react-ui-kit"
import "@your-org/react-ui-kit/styles.css"
function App() {
return (
<div className="p-8">
<Button variant="default" size="lg">
Primary Action
</Button>
<Button variant="outline" size="sm">
Secondary Action
</Button>
<Button variant="destructive" disabled>
Disabled Action
</Button>
</div>
)
}
โ Key Takeaways
- Start with solid foundations - TypeScript, testing, and accessibility from day one
- Use proven patterns - CVA for variants, Radix for complex interactions
- Automate everything - Testing, visual regression, and publishing
- Document thoroughly - Storybook stories serve as both docs and tests
- Think in systems - Consistent patterns across all components
This tutorial gives you a production-ready component library foundation. The real power comes from expanding it with more components while maintaining these patterns and standards.
๐ Additional Resources
Happy building! ๐