Shadcn Ui
/install shadcn-ui-pro
shadcn/ui Expert
Comprehensive guide for building production UIs with shadcn/ui, Tailwind CSS, react-hook-form, and zod.
Core Concepts
shadcn/ui is not a component library — it's a collection of copy-paste components built on Radix UI primitives. You own the code. Components are added to your project, not installed as dependencies.
Installation
# Initialize shadcn/ui in a Next.js project
npx shadcn@latest init
# Add individual components
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add select
npx shadcn@latest add table
npx shadcn@latest add toast
npx shadcn@latest add dropdown-menu
npx shadcn@latest add sheet
npx shadcn@latest add tabs
npx shadcn@latest add sidebar
# Add multiple at once
npx shadcn@latest add button card input label textarea select checkbox
Component Categories & When to Use
Layout & Navigation
| Component | Use When |
|---|---|
sidebar |
App-level navigation with collapsible sections |
navigation-menu |
Top-level site navigation with dropdowns |
breadcrumb |
Showing page hierarchy/location |
tabs |
Switching between related views in same context |
separator |
Visual divider between content sections |
sheet |
Slide-out panel (mobile nav, filters, detail views) |
resizable |
Adjustable panel layouts |
Forms & Input
| Component | Use When |
|---|---|
form |
Any form with validation (wraps react-hook-form) |
input |
Text, email, password, number inputs |
textarea |
Multi-line text input |
select |
Choosing from a list (native-like) |
combobox |
Searchable select (uses command + popover) |
checkbox |
Boolean or multi-select toggles |
radio-group |
Single selection from small set |
switch |
On/off toggle (settings, preferences) |
slider |
Numeric range selection |
date-picker |
Date selection (uses calendar + popover) |
toggle |
Pressed/unpressed state (toolbar buttons) |
Feedback & Overlay
| Component | Use When |
|---|---|
dialog |
Modal confirmation, forms, or detail views |
alert-dialog |
Destructive action confirmation ("Are you sure?") |
sheet |
Side panel for forms, filters, mobile nav |
toast |
Brief non-blocking notifications (via sonner) |
alert |
Inline status messages (info, warning, error) |
tooltip |
Hover hints for icons/buttons |
popover |
Rich content on click (color pickers, date pickers) |
hover-card |
Preview content on hover (user profiles, links) |
skeleton |
Loading placeholders |
progress |
Task completion indicators |
Data Display
| Component | Use When |
|---|---|
table |
Tabular data display |
data-table |
Tables with sorting, filtering, pagination (uses @tanstack/react-table) |
card |
Content containers with header, body, footer |
badge |
Status labels, tags, counts |
avatar |
User profile images |
accordion |
Collapsible FAQ or settings sections |
carousel |
Image/content slideshows |
scroll-area |
Custom scrollable containers |
Actions
| Component | Use When |
|---|---|
button |
Primary actions, form submissions |
dropdown-menu |
Context menus, action menus |
context-menu |
Right-click menus |
menubar |
Application menu bars |
command |
Command palette / search (⌘K) |
Form Patterns (react-hook-form + zod)
Complete Form Example
npx shadcn@latest add form input select textarea checkbox button
'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'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { toast } from 'sonner'
const formSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
role: z.enum(['admin', 'user', 'editor'], { required_error: 'Select a role' }),
bio: z.string().max(500).optional(),
notifications: z.boolean().default(false),
})
type FormValues = z.infer\x3Ctypeof formSchema>
export function UserForm() {
const form = useForm\x3CFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
bio: '',
notifications: false,
},
})
async function onSubmit(values: FormValues) {
try {
await createUser(values)
toast.success('User created successfully')
form.reset()
} catch (error) {
toast.error('Failed to create user')
}
}
return (
\x3CForm {...form}>
\x3Cform onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
\x3CFormField
control={form.control}
name="name"
render={({ field }) => (
\x3CFormItem>
\x3CFormLabel>Name\x3C/FormLabel>
\x3CFormControl>
\x3CInput placeholder="John Doe" {...field} />
\x3C/FormControl>
\x3CFormMessage />
\x3C/FormItem>
)}
/>
\x3CFormField
control={form.control}
name="email"
render={({ field }) => (
\x3CFormItem>
\x3CFormLabel>Email\x3C/FormLabel>
\x3CFormControl>
\x3CInput type="email" placeholder="[email protected]" {...field} />
\x3C/FormControl>
\x3CFormMessage />
\x3C/FormItem>
)}
/>
\x3CFormField
control={form.control}
name="role"
render={({ field }) => (
\x3CFormItem>
\x3CFormLabel>Role\x3C/FormLabel>
\x3CSelect onValueChange={field.onChange} defaultValue={field.value}>
\x3CFormControl>
\x3CSelectTrigger>
\x3CSelectValue placeholder="Select a role" />
\x3C/SelectTrigger>
\x3C/FormControl>
\x3CSelectContent>
\x3CSelectItem value="admin">Admin\x3C/SelectItem>
\x3CSelectItem value="editor">Editor\x3C/SelectItem>
\x3CSelectItem value="user">User\x3C/SelectItem>
\x3C/SelectContent>
\x3C/Select>
\x3CFormMessage />
\x3C/FormItem>
)}
/>
\x3CFormField
control={form.control}
name="bio"
render={({ field }) => (
\x3CFormItem>
\x3CFormLabel>Bio\x3C/FormLabel>
\x3CFormControl>
\x3CTextarea placeholder="Tell us about yourself..." {...field} />
\x3C/FormControl>
\x3CFormDescription>Max 500 characters\x3C/FormDescription>
\x3CFormMessage />
\x3C/FormItem>
)}
/>
\x3CFormField
control={form.control}
name="notifications"
render={({ field }) => (
\x3CFormItem className="flex flex-row items-start space-x-3 space-y-0">
\x3CFormControl>
\x3CCheckbox checked={field.value} onCheckedChange={field.onChange} />
\x3C/FormControl>
\x3Cdiv className="space-y-1 leading-none">
\x3CFormLabel>Email notifications\x3C/FormLabel>
\x3CFormDescription>Receive emails about account activity\x3C/FormDescription>
\x3C/div>
\x3C/FormItem>
)}
/>
\x3CButton type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Creating...' : 'Create User'}
\x3C/Button>
\x3C/form>
\x3C/Form>
)
}
Form with Server Action
'use client'
import { useFormState } from 'react-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
export function ContactForm() {
const form = useForm\x3CFormValues>({
resolver: zodResolver(schema),
})
async function onSubmit(values: FormValues) {
const formData = new FormData()
Object.entries(values).forEach(([key, value]) => formData.append(key, String(value)))
await submitContact(formData)
}
return (
\x3CForm {...form}>
\x3Cform onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* fields */}
\x3C/form>
\x3C/Form>
)
}
Theming & Dark Mode
Setup with next-themes
npm install next-themes
npx shadcn@latest add dropdown-menu
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
\x3CThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
\x3C/ThemeProvider>
)
}
// components/theme-toggle.tsx
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
\x3CDropdownMenu>
\x3CDropdownMenuTrigger asChild>
\x3CButton variant="outline" size="icon">
\x3CSun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
\x3CMoon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
\x3Cspan className="sr-only">Toggle theme\x3C/span>
\x3C/Button>
\x3C/DropdownMenuTrigger>
\x3CDropdownMenuContent align="end">
\x3CDropdownMenuItem onClick={() => setTheme('light')}>Light\x3C/DropdownMenuItem>
\x3CDropdownMenuItem onClick={() => setTheme('dark')}>Dark\x3C/DropdownMenuItem>
\x3CDropdownMenuItem onClick={() => setTheme('system')}>System\x3C/DropdownMenuItem>
\x3C/DropdownMenuContent>
\x3C/DropdownMenu>
)
}
Custom Colors 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%;
--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%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... etc */
}
}
Common Layouts
App Shell with Sidebar
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { AppSidebar } from '@/components/app-sidebar'
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
\x3CSidebarProvider>
\x3CAppSidebar />
\x3Cmain className="flex-1">
\x3Cheader className="flex h-14 items-center gap-4 border-b px-6">
\x3CSidebarTrigger />
\x3Ch1 className="text-lg font-semibold">Dashboard\x3C/h1>
\x3C/header>
\x3Cdiv className="p-6">{children}\x3C/div>
\x3C/main>
\x3C/SidebarProvider>
)
}
Responsive Header with Mobile Nav
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Menu } from 'lucide-react'
export function Header() {
return (
\x3Cheader className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
\x3Cdiv className="container flex h-14 items-center">
\x3Cdiv className="mr-4 hidden md:flex">
\x3CLogo />
\x3Cnav className="flex items-center gap-6 text-sm ml-6">
\x3CLink href="/dashboard">Dashboard\x3C/Link>
\x3CLink href="/settings">Settings\x3C/Link>
\x3C/nav>
\x3C/div>
{/* Mobile hamburger */}
\x3CSheet>
\x3CSheetTrigger asChild>
\x3CButton variant="outline" size="icon" className="md:hidden">
\x3CMenu className="h-5 w-5" />
\x3C/Button>
\x3C/SheetTrigger>
\x3CSheetContent side="left" className="w-[300px]">
\x3Cnav className="flex flex-col gap-4 mt-8">
\x3CLink href="/dashboard">Dashboard\x3C/Link>
\x3CLink href="/settings">Settings\x3C/Link>
\x3C/nav>
\x3C/SheetContent>
\x3C/Sheet>
\x3Cdiv className="flex flex-1 items-center justify-end gap-2">
\x3CThemeToggle />
\x3CUserMenu />
\x3C/div>
\x3C/div>
\x3C/header>
)
}
Card Grid
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
export function StatsGrid({ stats }: { stats: Stat[] }) {
return (
\x3Cdiv className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
\x3CCard key={stat.label}>
\x3CCardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
\x3CCardTitle className="text-sm font-medium">{stat.label}\x3C/CardTitle>
\x3Cstat.icon className="h-4 w-4 text-muted-foreground" />
\x3C/CardHeader>
\x3CCardContent>
\x3Cdiv className="text-2xl font-bold">{stat.value}\x3C/div>
\x3Cp className="text-xs text-muted-foreground">{stat.description}\x3C/p>
\x3C/CardContent>
\x3C/Card>
))}
\x3C/div>
)
}
Tailwind CSS Patterns
Common Utility Patterns
// Centering
\x3Cdiv className="flex items-center justify-center min-h-screen">
// Container with max-width
\x3Cdiv className="container mx-auto px-4">
// Responsive grid
\x3Cdiv className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
// Sticky header
\x3Cheader className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur">
// Truncated text
\x3Cp className="truncate">Very long text...\x3C/p>
// Line clamp
\x3Cp className="line-clamp-3">Multi-line truncation...\x3C/p>
// Aspect ratio
\x3Cdiv className="aspect-video rounded-lg overflow-hidden">
// Animations
\x3Cdiv className="animate-pulse"> {/* Loading skeleton */}
\x3Cdiv className="animate-spin"> {/* Spinner */}
\x3Cdiv className="transition-all duration-200 hover:scale-105">
Button Variants
\x3CButton>Default\x3C/Button>
\x3CButton variant="secondary">Secondary\x3C/Button>
\x3CButton variant="outline">Outline\x3C/Button>
\x3CButton variant="ghost">Ghost\x3C/Button>
\x3CButton variant="link">Link\x3C/Button>
\x3CButton variant="destructive">Delete\x3C/Button>
\x3CButton size="sm">Small\x3C/Button>
\x3CButton size="lg">Large\x3C/Button>
\x3CButton size="icon">\x3CPlus className="h-4 w-4" />\x3C/Button>
\x3CButton disabled>Disabled\x3C/Button>
\x3CButton asChild>\x3CLink href="/page">As Link\x3C/Link>\x3C/Button>
Toast Notifications
npx shadcn@latest add sonner
// app/layout.tsx
import { Toaster } from '@/components/ui/sonner'
export default function RootLayout({ children }) {
return (
\x3Chtml>\x3Cbody>{children}\x3CToaster />\x3C/body>\x3C/html>
)
}
// Usage anywhere
import { toast } from 'sonner'
toast.success('User created')
toast.error('Something went wrong')
toast.info('New update available')
toast.warning('This action cannot be undone')
toast.promise(asyncAction(), {
loading: 'Creating...',
success: 'Created!',
error: 'Failed to create',
})
Command Palette (⌘K)
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
export function CommandPalette() {
const [open, setOpen] = useState(false)
const router = useRouter()
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
return (
\x3CCommandDialog open={open} onOpenChange={setOpen}>
\x3CCommandInput placeholder="Type a command or search..." />
\x3CCommandList>
\x3CCommandEmpty>No results found.\x3C/CommandEmpty>
\x3CCommandGroup heading="Navigation">
\x3CCommandItem onSelect={() => { router.push('/dashboard'); setOpen(false) }}>
Dashboard
\x3C/CommandItem>
\x3CCommandItem onSelect={() => { router.push('/settings'); setOpen(false) }}>
Settings
\x3C/CommandItem>
\x3C/CommandGroup>
\x3C/CommandList>
\x3C/CommandDialog>
)
}
- Make sure OpenClaw is installed (local or Docker)
- Run the install command in chat:
/install shadcn-ui-pro - After installation, invoke the skill by name or use
/shadcn-ui-pro - Provide required inputs per the skill's parameter spec and get structured output
What is Shadcn Ui?
Use when building UI with shadcn/ui components, Tailwind CSS layouts, form patterns with react-hook-form and zod, theming, dark mode, sidebar layouts, mobile... It is an AI Agent Skill for Claude Code / OpenClaw, with 92 downloads so far.
How do I install Shadcn Ui?
Run "/install shadcn-ui-pro" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.
Is Shadcn Ui free?
Yes, Shadcn Ui is completely free, licensed under MIT-0. You can download, install and use it at no cost.
Which platforms does Shadcn Ui support?
Shadcn Ui is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).
Who created Shadcn Ui?
It is built and maintained by shenghoo123-png (@shenghoo123-png); the current version is v1.0.0.