| name | shadcn-ui |
| description | Expert guide for building accessible, customizable UI components with shadcn/ui, Radix UI, and Tailwind CSS. Covers installation, core components (Button, Input, Form, Card, Dialog, Select, Sheet, Table, Toast, Charts), forms with React Hook Form + Zod validation, theming with CSS variables, Next.js integration, and advanced patterns. |
| metadata | {"version":"1.0.0","source":"https://github.com/giuseppe-trisciuoglio/developer-kit","related_skills":["react-best-practices","frontend-design","web-design-guidelines"]} |
shadcn/ui Component Patterns
Overview
Expert guide for building accessible, customizable UI components with shadcn/ui, Radix UI, and Tailwind CSS. This skill provides comprehensive patterns for implementing production-ready components with full accessibility support.
Table of Contents
- When to Use
- Quick Start
- Installation & Setup
- Project Configuration
- Core Components
- Button
- Input & Form Fields
- Forms with Validation
- Card
- Dialog (Modal)
- Select (Dropdown)
- Sheet (Slide-over)
- Menubar & Navigation
- Table
- Toast Notifications
- Charts
- Advanced Patterns
- Customization
- Next.js Integration
- Best Practices
- Common Component Combinations
When to Use
- Setting up a new project with shadcn/ui
- Installing or configuring individual components
- Building forms with React Hook Form and Zod validation
- Creating accessible UI components (buttons, dialogs, dropdowns, sheets)
- Customizing component styling with Tailwind CSS
- Implementing design systems with shadcn/ui
- Building Next.js applications with TypeScript
- Creating complex layouts and data displays
Instructions
- Initialize Project: Run
npx shadcn@latest init to configure shadcn/ui
- Install Components: Add components with
npx shadcn@latest add <component>
- Configure Theme: Customize CSS variables in globals.css for theming
- Import Components: Use components from
@/components/ui/ directory
- Customize as Needed: Modify component code directly in your project
- Add Form Validation: Integrate React Hook Form with Zod schemas
- Test Accessibility: Verify ARIA attributes and keyboard navigation
Quick Start
For new projects, use the automated setup:
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npx shadcn@latest init
npx shadcn@latest add button input form card dialog select
For existing projects:
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
npx shadcn@latest init
What is shadcn/ui?
shadcn/ui is not a traditional component library or npm package. Instead:
- It's a collection of reusable components that you can copy into your project
- Components are yours to customize - you own the code
- Built with Radix UI primitives for accessibility
- Styled with Tailwind CSS utilities
- Includes CLI tool for easy component installation
Installation & Setup
Initial Setup
npx shadcn@latest init
During setup, you'll configure:
- TypeScript or JavaScript
- Style (Default, New York, etc.)
- Base color theme
- CSS variables or Tailwind CSS classes
- Component installation path
Installing Individual Components
npx shadcn@latest add button
npx shadcn@latest add button input form
npx shadcn@latest add --all
Core Components
Button Component
npx shadcn@latest add button
import { Button } from "@/components/ui/button";
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button =>Large
Input & Form Fields
npx shadcn@latest add input label
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
<div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="email">Email</Label>
<Input type="email" id="email" placeholder="Email" />
</div>
Forms with Validation
npx shadcn@latest add form
This installs React Hook Form, Zod, and form components.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { 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"
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
})
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
: { : , : },
})
() {
.(values)
}
(
)
}
Card Component
npx shadcn@latest add card
import {
Card, CardContent, CardDescription,
CardFooter, CardHeader, CardTitle,
} from "@/components/ui/card"
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one-click.</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
</div>
</form>
</CardContent>
< =>
Cancel
Deploy
</>
Dialog (Modal)
npx shadcn@latest add dialog
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input id="name" value="Pedro Duarte" />
</div>
<DialogFooter>
<Button type="submit">Save changes
</>
Select (Dropdown)
npx shadcn@latest add select
import {
Select, SelectContent, SelectItem,
SelectTrigger, SelectValue,
} from "@/components/ui/select"
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectContent>
</Select>
Sheet (Slide-over)
npx shadcn@latest add sheet
import {
Sheet, SheetContent, SheetDescription,
SheetHeader, SheetTitle, SheetTrigger,
} from "@/components/ui/sheet"
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Sheet</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
<SheetDescription>Configure your application.</SheetDescription>
</SheetHeader>
{/* Settings content */}
</SheetContent>
</Sheet>
Table
npx shadcn@latest add table
import {
Table, TableBody, TableCaption, TableCell,
TableHead, TableHeader, TableRow,
} from "@/components/ui/table"
<Table>
<TableCaption>A list of recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</>
Toast Notifications
npx shadcn@latest add toast
Setup in root layout:
import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}
Usage:
import { useToast } from "@/components/ui/use-toast"
const { toast } = useToast()
toast({ title: "Success", description: "Changes saved." })
toast({ variant: "destructive", title: "Error", description: "Something went wrong." })
toast({
title: "Uh oh!",
description: "Something went wrong.",
action: <ToastAction altText="Try again">Try again</ToastAction>,
})
Charts
npx shadcn@latest add chart
Built on Recharts with consistent theming:
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import { ChartContainer, ChartTooltipContent } from "@/components/ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
} satisfies import("@/components/ui/chart").ChartConfig
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar = = = />
} />
</>
Theming with CSS Variables
Configure in globals.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-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: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
: ;
: ;
}
}
Customizing Components
Since you own the code, customize directly with cva:
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
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",
custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
xl: "h-14 rounded-md px-10 text-lg",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
Next.js Integration
App Router
Ensure interactive components use "use client" directive. Add Toaster to root layout.
Server Components
Wrap interactive shadcn/ui components in Client Components when used in Server Components.
Server Actions with Forms
"use client"
async function onSubmit(values: z.infer<typeof formSchema>) {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("Failed to submit")
toast({ title: "Success!", description: "Message sent." })
}
Constraints and Warnings
- Not an NPM Package: Components are copied to your project; you own the code
- Client Components: Most components require
"use client" directive
- Radix Dependencies: Ensure all
@radix-ui packages are installed
- Tailwind Required: Components rely on Tailwind CSS utilities
- TypeScript: Designed for TypeScript projects; type definitions included
- Path Aliases: Configure
@ alias in tsconfig.json for imports
- Dark Mode: Set up dark mode with CSS variables or class strategy
Best Practices
- Accessibility: Components use Radix UI primitives for ARIA compliance
- Customization: Modify components directly in your codebase
- Type Safety: Use TypeScript for type-safe props and state
- Validation: Use Zod schemas for form validation
- Styling: Leverage Tailwind utilities and CSS variables
- Consistency: Use the same component patterns across your app
- Testing: Components are testable with React Testing Library
- Performance: Components are optimized and tree-shakeable
References