shadcnui-advanced-patterns
Use this skill for advanced shadcn/ui integration patterns including forms with validation, data tables, performance optimization, testing, and migration from other component libraries. Assumes shadcn is already set up with components installed.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
shadcn/ui Advanced Patterns
Advanced patterns for building complex features with shadcn/ui.
Prerequisites: Your project must have:
- Completed
shadcn initsetup - Components installed and working
- Familiarity with React and TypeScript
- Basic understanding of component customization
Forms with Validation
Setup
npx shadcn@latest add form input button label
npm install react-hook-form @hookform/resolvers zod
Simple Form Example
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
// Define schema
const formSchema = z.object({
username: z.string().min(2, 'Username must be at least 2 characters'),
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
type FormValues = z.infer<typeof formSchema>
export function MyForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
email: '',
password: '',
},
})
function onSubmit(values: FormValues) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="john_doe" {...field} />
</FormControl>
<FormDescription>Your unique username</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@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">Submit</Button>
</form>
</Form>
)
}
Data Tables
Setup
npx shadcn@latest add table
npm install @tanstack/react-table
Basic Table Example
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
interface User {
id: string
name: string
email: string
status: 'active' | 'inactive'
}
const users: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', status: 'active' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', status: 'active' },
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', status: 'inactive' },
]
export function UsersTable() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell className="capitalize">{user.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
Advanced Table with TanStack Table
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { ColumnDef } from '@tanstack/react-table'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
interface User {
id: string
name: string
email: string
status: 'active' | 'inactive'
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => <span className="capitalize">{row.getValue('status')}</span>,
},
]
export function AdvancedTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}
Modal Dialogs
Simple Dialog
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
export function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Delete Item</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<div className="flex gap-4">
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
</div>
</DialogContent>
</Dialog>
)
}
Dropdown Menus
Setup
npx shadcn@latest add dropdown-menu
Example
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
export function UserMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
Notifications/Toast
Setup
npx shadcn@latest add toaster
npm install sonner
Usage
import { Toaster, toast } from 'sonner'
export function App() {
return (
<>
<Toaster />
<button
onClick={() =>
toast.success('Action completed successfully!', {
description: 'Your changes have been saved.',
})
}
>
Show Toast
</button>
</>
)
}
Controlled Components
Tabs Component
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
export function TabsExample() {
const [activeTab, setActiveTab] = useState('tab1')
return (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content for tab 1</TabsContent>
<TabsContent value="tab2">Content for tab 2</TabsContent>
</Tabs>
)
}
Performance Optimization
Memoization
import { memo } from 'react'
// Prevent re-renders when props haven't changed
const CardItem = memo(function CardItem({ item }: { item: Item }) {
return <Card>{item.name}</Card>
})
Code Splitting
Import components only when needed:
import { lazy, Suspense } from 'react'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
export function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)
}
Virtual Scrolling for Long Lists
import { FixedSizeList as List } from 'react-window'
export function LargeList({ items }: { items: string[] }) {
return (
<List height={400} itemCount={items.length} itemSize={35} width="100%">
{({ index, style }) => <div style={style}>{items[index]}</div>}
</List>
)
}
Testing Components
Unit Test Example
import { render, screen } from '@testing-library/react'
import { Button } from '@/components/ui/button'
describe('Button', () => {
it('renders button with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
screen.getByRole('button').click()
expect(handleClick).toHaveBeenCalled()
})
})
Form Testing Example
import { render, screen, userEvent } from '@testing-library/react'
import { MyForm } from './MyForm'
it('submits form with valid data', async () => {
const user = userEvent.setup()
render(<MyForm />)
await user.type(screen.getByLabelText(/username/i), 'john_doe')
await user.type(screen.getByLabelText(/email/i), 'john@example.com')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /submit/i }))
// Assert form submission happened
})
Migration from Other Libraries
From Material-UI
// Material-UI
import { Button as MuiButton } from '@mui/material'
// shadcn/ui
import { Button } from '@/components/ui/button'
// Variants map similarly
<Button variant="contained" /> // MUI → <Button /> (default)
<Button variant="outlined" /> // MUI → <Button variant="outline" />
<Button variant="text" /> // MUI → <Button variant="ghost" />
From Chakra UI
// Chakra
import { Button, Box } from '@chakra-ui/react'
// shadcn/ui
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
// Props are similar
<Button colorScheme="blue" /> // Chakra → <Button /> (use CSS variables instead)
<Box padding={4} /> // Chakra → <div className="p-4" /> (Tailwind)
Monorepo Setup
If using shadcn in a monorepo with Turbo:
# In app package
npx shadcn@latest init
# In shared UI package
npx shadcn@latest init
# Components go in @repo/ui/src/components
Share components via package:
// In app
import { Button } from '@repo/ui/components'
Key Patterns Summary
- Forms - react-hook-form + zod for validation
- Tables - @tanstack/react-table for advanced features
- Dialogs - Dialog + state management
- Lists - Virtual scrolling for performance
- Navigation - Tabs, Pagination, Breadcrumbs
- Testing - @testing-library/react for unit tests
- Performance - Memoization and code splitting
- Accessibility - Components built with Radix for A11y
Resources
- react-hook-form: https://react-hook-form.com
- TanStack Table: https://tanstack.com/table/latest
- Testing Library: https://testing-library.com
- Sonner (Toast): https://sonner.emilkowal.ski
- shadcn/ui Docs: https://ui.shadcn.com
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!