Getting Started with Shadcn UI for Modern React Applications
What is Shadcn UI?
Hello everyone! I'm Shailesh Chaudhari, a full-stack developer who has built numerous user interfaces using various component libraries. Today, I'll guide you through Shadcn UI, a modern UI component library that has revolutionized how we build React applications.
Shadcn UI is not just another component library—it's a collection of reusable components built on top of Radix UI and Tailwind CSS. Unlike traditional UI libraries, Shadcn UI gives you full control over your components by copying the source code directly into your project, allowing for complete customization without external dependencies.
Key Features of Shadcn UI
- Copy & Paste Components: Source code is copied to your project
- Built on Radix UI: Accessible primitives with keyboard navigation
- Tailwind CSS Integration: Utility-first styling approach
- TypeScript Support: Full type safety out of the box
- Customizable: Easy to modify and extend components
- Accessible: WCAG compliant components
- Modern Design: Clean, contemporary aesthetics
Installation and Setup
Prerequisites
Before installing Shadcn UI, ensure you have:
- Next.js project (or any React project)
- Tailwind CSS configured
- TypeScript (recommended)
- Node.js 16+
Installing Shadcn UI CLI
npx shadcn-ui@latest init
This will prompt you to configure your project:
Would you like to use TypeScript (recommended)? 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 located? › tailwind.config.js
Configure the import alias for components? › @/components
Configure the import alias for utils? › @/lib/utils
Project Structure After Installation
your-project/
├── components/
│ ├── ui/ # Shadcn UI components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── ...
│ └── ...
├── lib/
│ └── utils.ts # Utility functions
├── app/
│ └── globals.css # Updated with CSS variables
└── tailwind.config.js # Updated with Shadcn config
Adding Individual Components
# Add specific components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add dialog
# Add multiple components at once
npx shadcn-ui@latest add button card input dialog
Core Components
Button Component
// components/ui/button.tsx (generated by CLI)
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 }
Using the Button Component
// Basic usage
import { Button } from "@/components/ui/button"
export default function MyComponent() {
return (
<div className="space-x-4">
<Button>Default Button</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<svg className="h-4 w-4" />
</Button>
</div>
)
}
Card Component
// components/ui/card.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
Using the Card Component
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export default function ProductCard({ product }) {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>{product.name}</CardTitle>
<CardDescription>{product.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$${product.price}</div>
<p className="text-sm text-muted-foreground">
{product.features.join(", ")}
</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Learn More</Button>
<Button>Buy Now</Button>
</CardFooter>
</Card>
)
}
Form Components
Input Component
// components/ui/input.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
Label Component
// components/ui/label.tsx
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
Building a Complete Form
// components/ContactForm.tsx
'use client'
import { useState } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('Form submitted:', formData)
// Handle form submission
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}))
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Contact Us</CardTitle>
<CardDescription>
Send us a message and we'll get back to you.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<textarea
id="message"
name="message"
placeholder="Your message..."
value={formData.message}
onChange={handleChange}
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required
/>
</div>
<Button type="submit" className="w-full">
Send Message
</Button>
</form>
</CardContent>
</Card>
)
}
Advanced Components
Dialog (Modal) Component
// components/ui/dialog.tsx
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
Using Dialog Component
// components/ConfirmDialog.tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
interface ConfirmDialogProps {
title: string
description: string
confirmText?: string
cancelText?: string
onConfirm: () => void
children: React.ReactNode
}
export function ConfirmDialog({
title,
description,
confirmText = "Confirm",
cancelText = "Cancel",
onConfirm,
children,
}: ConfirmDialogProps) {
return (
<Dialog>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">{cancelText}</Button>
<Button onClick={onConfirm}>{confirmText}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
// Usage
<ConfirmDialog
title="Delete Item"
description="Are you sure you want to delete this item? This action cannot be undone."
onConfirm={handleDelete}
>
<Button variant="destructive">Delete</Button>
</ConfirmDialog>
Theming and Customization
CSS Variables for Theming
// app/globals.css
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--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 {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
Custom Theme Configuration
// lib/themes.ts
export const themes = {
light: {
background: '0 0% 100%',
foreground: '222.2 84% 4.9%',
primary: '221.2 83.2% 53.3%',
// ... other colors
},
dark: {
background: '222.2 84% 4.9%',
foreground: '210 40% 98%',
primary: '217.2 91.2% 59.8%',
// ... other colors
},
blue: {
background: '0 0% 100%',
foreground: '222.2 84% 4.9%',
primary: '217.2 91.2% 59.8%',
// ... custom blue theme
}
}
// Theme provider component
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'light' | 'dark' | 'blue'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Theme
if (savedTheme) {
setTheme(savedTheme)
document.documentElement.classList.toggle('dark', savedTheme === 'dark')
}
}, [])
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
// Apply custom theme colors
const root = document.documentElement
const themeColors = themes[newTheme]
Object.entries(themeColors).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value)
})
}
return (
<ThemeContext.Provider value={{ theme, setTheme: handleThemeChange }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
Building Complex UI Patterns
Data Table Component
// components/ui/table.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
Using Table Component
// components/DataTable.tsx
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface User {
id: string
name: string
email: string
role: string
}
interface DataTableProps {
users: User[]
}
export function DataTable({ users }: DataTableProps) {
return (
<Table>
<TableCaption>A list of users in your system.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
Edit
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
Best Practices and Performance
Component Organization
// components/
├── ui/ # Shadcn UI components
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ └── ...
├── forms/ # Form components
│ ├── login-form.tsx
│ ├── signup-form.tsx
│ └── ...
├── layout/ # Layout components
│ ├── header.tsx
│ ├── sidebar.tsx
│ └── ...
├── sections/ # Page sections
│ ├── hero.tsx
│ ├── features.tsx
│ └── ...
└── index.ts # Barrel exports
Tree Shaking and Bundle Optimization
// Only import what you need
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
// Avoid importing entire libraries
// ❌ Bad
import * as UI from "@/components/ui"
// ✅ Good - tree shaking friendly
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Accessibility Considerations
// Always include proper ARIA labels
<Button aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// Use semantic HTML
<nav aria-label="Main navigation">
<Button variant="ghost">Home</Button>
</nav>
// Ensure keyboard navigation
// Shadcn components handle this automatically with Radix UI
Testing Shadcn Components
Component Testing
// __tests__/Button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from '@/components/ui/button'
describe('Button', () => {
it('renders with default props', () => {
render(<Button>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
expect(button).toBeInTheDocument()
expect(button).toHaveClass('bg-primary')
})
it('handles click events', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
await user.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('applies variant classes correctly', () => {
render(<Button variant="destructive">Delete</Button>)
const button = screen.getByRole('button', { name: /delete/i })
expect(button).toHaveClass('bg-destructive')
})
})
Production Deployment
Build Optimization
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable SWC minification
swcMinify: true,
// Optimize CSS
experimental: {
optimizeCss: true,
},
// Bundle analyzer (conditionally)
...(process.env.ANALYZE === 'true' && {
bundleAnalyzer: {
enabled: true,
openAnalyzer: true,
},
}),
}
module.exports = nextConfig
Performance Monitoring
// lib/performance.ts
export function measureComponentRender(componentName: string) {
if (process.env.NODE_ENV === 'development') {
console.time(`Render ${componentName}`)
return () => console.timeEnd(`Render ${componentName}`)
}
return () => {}
}
// Usage in components
const endMeasure = measureComponentRender('Button')
// ... component logic
endMeasure()
Conclusion: Mastering Shadcn UI
Shadcn UI represents a paradigm shift in how we approach component libraries in React applications. By copying source code directly into your project, Shadcn UI gives you complete control over your components while maintaining the benefits of a well-designed component system.
The combination of Radix UI primitives, Tailwind CSS styling, and TypeScript provides a robust foundation for building accessible, performant, and maintainable user interfaces. The utility-first approach ensures that your components are both flexible and consistent.
As you build more complex applications, you'll appreciate how Shadcn UI scales with your needs. The component architecture encourages composition, making it easy to build complex UI patterns from simple, reusable parts.
Remember that great UI design is about more than just visual appeal—it's about creating intuitive, accessible experiences that work seamlessly across all devices and user needs. Shadcn UI provides the tools to achieve this balance.
"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs
"With Shadcn UI, your components work beautifully and feel right at home." - Shailesh Chaudhari