Component Library Guide with Shadcn/UI
Status: Complete Implementation Guide
Version: 2.0
Purpose: Building production-ready component libraries using Shadcn/UI
Framework: Shadcn/UI + Radix UI + Tailwind CSS
Overview
This guide provides a comprehensive approach to implementing component libraries using Shadcn/UI, our preferred component system. Based on lessons learned from real MVP implementations, this guide emphasizes working components over perfect abstractions.
Related Guides:
- UI Architecture Guide - Complete Shadcn/UI philosophy and setup
- Autonomous MVP Lessons - Critical implementation patterns
- Design-First Development Standards - Visual validation requirements
Why Shadcn/UI?
Based on our V3 vs V4 MVP analysis:
- Copy-paste ownership: Full control over component code
- No black boxes: Every line of code is visible and modifiable
- Built on Radix UI: Accessibility guaranteed out of the box
- Real implementations: Components that work with actual data
Quick Start
Detailed Setup: See UI Architecture Guide for complete installation instructions.
Step 1: Initialize Shadcn/UI
# Create your Next.js or Vite project first
npx create-next-app@latest my-app --typescript --tailwind --app
# Initialize shadcn/ui
npx shadcn-ui@latest init
# Answer the prompts:
# β Would you like to use TypeScript? Yes
# β Which style would you like to use? Default
# β Which color would you like to use as base color? Slate
# β Where is your global CSS file? app/globals.css
# β Would you like to use CSS variables for colors? Yes
# β Where is your tailwind.config.js? tailwind.config.js
# β Configure the import alias? components: @/components, utils: @/lib/utils
Step 2: Install Core Components
Critical Lesson: As documented in Autonomous MVP Lessons, install only components you'll immediately use with real data.
# Install essential components first
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add form
npx shadcn-ui@latest add table
# These create files in your project:
# components/ui/button.tsx
# components/ui/card.tsx
# components/ui/input.tsx
# etc.
Step 3: Component Structure
Architecture Pattern: Following Component Library Design System Policy, organize by atomic design:
src/
βββ components/
β βββ ui/ # Shadcn/UI components (atoms)
β β βββ button.tsx
β β βββ card.tsx
β β βββ input.tsx
β β βββ form.tsx
β βββ features/ # Business components (molecules/organisms)
β β βββ campaign-list.tsx
β β βββ contact-form.tsx
β β βββ analytics-dashboard.tsx
β βββ layouts/ # Page templates
β βββ app-layout.tsx
β βββ auth-layout.tsx
βββ lib/
βββ utils.ts # Shadcn/UI utilities
Real Implementation Examples
Critical: Following Autonomous MVP Lessons, every example shows REAL data integration, not mocks.
Example 1: Campaign List with Real Data
// src/components/features/campaign-list.tsx
import { useEffect, useState } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { MoreHorizontal, Mail, Users, TrendingUp } from 'lucide-react'
// REAL database types
interface Campaign {
id: string // Real UUID from database
name: string
status: 'draft' | 'active' | 'completed'
sent_count: number
open_rate: number
created_at: string
}
export function CampaignList() {
const [campaigns, setCampaigns] = useState<Campaign[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// REAL API call - no mocks!
fetch('/api/campaigns')
.then(res => res.json())
.then(data => {
setCampaigns(data.campaigns)
setLoading(false)
})
}, [])
// Verify real data (lesson from V4)
console.log('Real campaign IDs:', campaigns.map(c => c.id))
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Email Campaigns</span>
<Button>
<Mail className="mr-2 h-4 w-4" />
New Campaign
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Campaign</TableHead>
<TableHead>Status</TableHead>
<TableHead>Sent</TableHead>
<TableHead>Open Rate</TableHead>
<TableHead className="w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{campaigns.map((campaign) => (
<TableRow key={campaign.id}>
<TableCell className="font-medium">{campaign.name}</TableCell>
<TableCell>
<Badge variant={campaign.status === 'active' ? 'default' : 'secondary'}>
{campaign.status}
</Badge>
</TableCell>
<TableCell>{campaign.sent_count.toLocaleString()}</TableCell>
<TableCell>{campaign.open_rate}%</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
Example 2: Form with Validation
Testing Integration: See Testing Framework Guide for testing form components.
// src/components/features/contact-form.tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { toast } from '@/components/ui/use-toast'
// Real validation schema
const formSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name must be at least 2 characters'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
export function ContactForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
name: '',
message: '',
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
// REAL API call
const response = await fetch('/api/contacts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
const data = await response.json()
// Verify real database record created
console.log('Contact created with ID:', data.id)
toast({
title: 'Contact saved',
description: `Contact ${data.id} has been added to your list.`,
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormDescription>
We'll never share your email with anyone else.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about your needs..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Design System Integration
Design Resources:
- Design Token System Methodology - Token architecture
- Design-First Development Standards - Implementation order
Customizing Shadcn/UI Theme
// tailwind.config.ts - Real theme customization
import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: ["class"],
content: [
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
// Custom brand colors (following design-first standards)
brand: {
50: '#f0f9ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
// Semantic colors for real UI states
success: {
DEFAULT: '#059669',
foreground: '#ffffff',
},
warning: {
DEFAULT: '#d97706',
foreground: '#ffffff',
},
},
// Real spacing system
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
plugins: [require("tailwindcss-animate")],
}
export default config
CSS Variables for Dynamic Theming
/* app/globals.css - Working theme system */
@layer base {
:root {
/* Light theme - tested with real components */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
/* Dark theme - production tested */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... additional dark theme variables ... */
}
}
Accessibility Built-In
Complete Guide: See Accessibility Implementation Guide for comprehensive WCAG compliance strategies.
Radix UI Benefits
All Shadcn/UI components use Radix UI primitives, providing:
// Automatic accessibility features
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
export function AccessibleModal() {
return (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Contact</DialogTitle>
<DialogDescription>
{/* Automatically read by screen readers */}
Make changes to your contact here. Click save when you're done.
</DialogDescription>
</DialogHeader>
{/* Focus trapped, escape key handled, ARIA attributes automatic */}
</DialogContent>
</Dialog>
)
}
Built-in Features
- Keyboard Navigation: Tab, Arrow keys, Escape - all handled
- Focus Management: Focus trap, restore, and ring indicators
- ARIA Attributes: Proper roles, states, and properties
- Screen Reader: Announcements and descriptions included
Testing Shadcn/UI Components
Testing Guide: See Testing Framework Guide for complete testing strategies.
Testing Real Components
// __tests__/campaign-list.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { CampaignList } from '@/components/features/campaign-list'
import { server } from '@/mocks/server'
import { rest } from 'msw'
describe('CampaignList', () => {
it('displays real campaign data', async () => {
// Mock real API response
server.use(
rest.get('/api/campaigns', (req, res, ctx) => {
return res(ctx.json({
campaigns: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'Welcome Series',
status: 'active',
sent_count: 1250,
open_rate: 45.2
}
]
}))
})
)
render(<CampaignList />)
// Wait for real data
await waitFor(() => {
expect(screen.getByText('Welcome Series')).toBeInTheDocument()
})
// Verify real UUID displayed
const rows = screen.getAllByRole('row')
expect(rows).toHaveLength(2) // header + 1 data row
})
})
Common Patterns
Forms with API Integration
Related: API Integration Guide for backend patterns
// Pattern: Form -> API -> Database -> Feedback
export function CreateCampaignForm() {
const [loading, setLoading] = useState(false)
async function handleSubmit(data: FormData) {
setLoading(true)
// Real API integration
const response = await fetch('/api/campaigns', {
method: 'POST',
body: JSON.stringify(data)
})
const campaign = await response.json()
// Navigate to real campaign
router.push(`/campaigns/${campaign.id}`)
}
return (
<Form onSubmit={handleSubmit}>
{/* Form fields */}
<Button type="submit" disabled={loading}>
{loading ? <Loader2 className="animate-spin" /> : 'Create Campaign'}
</Button>
</Form>
)
}
Data Tables with Filtering
Related: SaaS Multi-tenancy Guide for data isolation patterns
// Using TanStack Table with Shadcn/UI
import { useReactTable, getCoreRowModel, getFilteredRowModel } from '@tanstack/react-table'
export function ContactsTable({ contacts }: { contacts: Contact[] }) {
const table = useReactTable({
data: contacts,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
return (
<div>
<Input
placeholder="Filter contacts..."
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
<Table>
{/* Render table with real data */}
</Table>
</div>
)
}
Navigation Patterns
Related: User Journey Navigation Standards for UX patterns
// Lesson from V4: Make ALL features discoverable
export function AppNavigation() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Campaigns</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="/campaigns">All Campaigns</NavigationMenuLink>
<NavigationMenuLink href="/campaigns/new">Create New</NavigationMenuLink>
<NavigationMenuLink href="/campaigns/templates">Templates</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
{/* Every feature has a visible entry point */}
</NavigationMenuList>
</NavigationMenu>
)
}
Migration from Custom Components
π§Ή Related: MVP Version Cleanup Guide for migration strategies
Step-by-Step Migration
Audit Existing Components
# Find all custom components find src/components -name "*.tsx" -type fMap to Shadcn/UI Equivalents
// Old custom button import { CustomButton } from './components/CustomButton' // New Shadcn/UI button import { Button } from '@/components/ui/button'Update Props and Styling
// Old: Custom props <CustomButton color="primary" size="large" /> // New: Shadcn/UI variants <Button variant="default" size="lg" />Test with Real Data
// Ensure migrated components work with real APIs const { data } = await fetch('/api/real-endpoint')
Best Practices & Lessons Learned
Do's
Start with Working Examples
- Every component must demonstrate real functionality
- No placeholder data or mock implementations
Use Shadcn/UI CLI
# Always use CLI to add components npx shadcn-ui@latest add [component]Customize Thoughtfully
- Modify copied components directly
- Keep customizations in version control
Test with Production Data
- Components must handle real API responses
- Test error states with actual failures
Don'ts
Don't Over-Abstract
- Avoid creating wrapper components unnecessarily
- Use Shadcn/UI components directly
Don't Mock in Production
- No fake data in component examples
- Real integrations from day one
Don't Ignore Accessibility
- Radix UI handles basics, but test with screen readers
- Verify keyboard navigation works
Additional Resources
Shadcn/UI Ecosystem
Related Internal Guides
- UI Architecture Guide - Complete setup and philosophy
- Autonomous MVP Lessons - Real-world patterns
- Design-First Standards - Visual requirements
- Testing Framework - Component testing
Community Resources
Success Metrics
Based on our V4 implementation success:
- Real Data Integration: 100% of components work with actual APIs
- Feature Discoverability: All features accessible via navigation
- Accessibility Score: WCAG 2.1 AA compliant out of the box
- Developer Velocity: 70% faster development with copy-paste
- Bundle Size: Only includes used components
This guide transforms component development from building custom solutions to leveraging battle-tested, accessible components that work with real data from day one. Follow the cross-referenced guides for deep dives into specific topics.