Module 3: UI Components
Build consistent, accessible UI with component libraries.
shadcn/ui (Recommended)
Not a component library - copy/paste components you own and customize.
Setup
bash
npx shadcn@latest initSelect options:
- TypeScript: Yes
- Style: Default
- Base color: Slate
- CSS variables: Yes
Add Components
bash
# Add specific components
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
# Add multiple
npx shadcn@latest add button card input formUsage
tsx
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
export function MyComponent() {
return (
<Card>
<CardHeader>
<CardTitle>Sign Up</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input placeholder="Email" type="email" />
<Input placeholder="Password" type="password" />
<Button className="w-full">Create Account</Button>
</CardContent>
</Card>
)
}Button Variants
tsx
<Button variant="default">Primary</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="default">Default</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/somewhere">Link Button</Link>
</Button>Forms with React Hook Form
bash
npx shadcn@latest add form
npm install react-hook-form @hookform/resolvers zodtsx
'use client'
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,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
export function SignUpForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Sign Up
</Button>
</form>
</Form>
)
}Dialog/Modal
tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
export function DeleteDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2">
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
</div>
</DialogContent>
</Dialog>
)
}Data Table
bash
npx shadcn@latest add table
npm install @tanstack/react-tabletsx
'use client'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
interface User {
id: string
name: string
email: string
role: string
}
export function UsersTable({ users }: { users: User[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}Common Patterns
Loading States
tsx
import { Loader2 } from 'lucide-react'
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>Toast Notifications
bash
npx shadcn@latest add toasttsx
// In your layout
import { Toaster } from '@/components/ui/toaster'
export default function Layout({ children }) {
return (
<>
{children}
<Toaster />
</>
)
}
// Usage
import { useToast } from '@/components/ui/use-toast'
function MyComponent() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: 'Success!',
description: 'Your changes have been saved.',
})
}}
>
Save
</Button>
)
}Summary
- shadcn/ui - Copy/paste components you own
- Radix UI - Unstyled accessible primitives underneath
- React Hook Form + Zod - Type-safe form validation
- Tailwind - Utility classes for styling