Design and build modern UI components and pages using shadcn/ui. Creates clean, accessible interfaces with Tailwind CSS following shadcn principles...
Build production-ready UI components using shadcn/ui principles: minimal, accessible, composable, and beautiful by default.
Design modern, minimal interfaces with:
Build composable components that work together seamlessly.
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export function MyComponent() {
return (
<div className="container mx-auto p-6 space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Title</h1>
<p className="text-sm text-muted-foreground">Description</p>
</div>
<Card className="shadow-sm">
<CardHeader>
<CardTitle>Section</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Content here */}
</CardContent>
</Card>
</div>
)
}
text-2xl (headings) → text-base (body) → text-sm (secondary)font-semibold (600) for emphasis, font-medium (500) for labels, font-normal (400) for bodytext-foreground (primary), text-muted-foreground (secondary)<h1 className="text-2xl font-semibold">Page Title</h1>
<p className="text-muted-foreground">Supporting text</p>
Use consistent spacing scale:
space-y-2 (8px) - within sectionsspace-y-4 (16px) - between elementsspace-y-6 (24px) - between sectionsspace-y-8 (32px) - major divisions<div className="container mx-auto p-6 space-y-6">
<section className="space-y-4">
<div className="space-y-2">
{/* Related elements */}
</div>
</section>
</div>
Use semantic color tokens:
bg-background, bg-card, bg-mutedtext-foreground, text-muted-foregroundborder-border, border-inputbg-primary, text-primary-foregroundbg-destructive, text-destructive-foreground<Card className="bg-card text-card-foreground border-border">
<Button className="bg-primary text-primary-foreground">
Primary Action
</Button>
<div className="bg-muted/50 text-muted-foreground">
Subtle highlight
</div>
</Card>
Three levels only:
shadow-sm: Cards, raised sections (0 1px 2px)shadow-md: Dropdowns, popovers (0 4px 6px)shadow-lg: Modals, dialogs (0 10px 15px)<Card className="shadow-sm hover:shadow-md transition-shadow" />
<Button className="transition-colors duration-200 hover:bg-primary/90">
<Card className="transition-all duration-200 hover:shadow-md hover:scale-[1.02]">
Always include:
<main>, <nav>, <article>):focus-visible:ring-2)<button
aria-label="Close dialog"
className="focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>
<X className="h-4 w-4" />
</button>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{stats.map(stat => (
<Card key={stat.id} className="shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">
{stat.label}
</CardTitle>
{stat.icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">
{stat.change}
</p>
</CardContent>
</Card>
))}
</div>
<form className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
placeholder="Enter your name"
className="max-w-md"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="name@example.com"
className="max-w-md"
/>
</div>
</div>
<div className="flex gap-3">
<Button type="submit">Submit</Button>
<Button type="button" variant="outline">Cancel</Button>
</div>
</form>
<Card className="shadow-sm">
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Order</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map(order => (
<TableRow
key={order.id}
className="hover:bg-muted/50 transition-colors"
>
<TableCell className="font-medium">{order.id}</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell>
<Badge variant={order.statusVariant}>
{order.status}
</Badge>
</TableCell>
<TableCell className="text-right">
{order.amount}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when done.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Form fields */}
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
// Skeleton loading
<Card className="shadow-sm">
<CardHeader>
<Skeleton className="h-4 w-[200px]" />
</CardHeader>
<CardContent className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[80%]" />
</CardContent>
</Card>
// Loading button
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
<Card className="shadow-sm">
<CardContent className="flex flex-col items-center justify-center py-12 space-y-3">
<div className="p-4 bg-muted rounded-full">
<FileX className="h-8 w-8 text-muted-foreground" />
</div>
<div className="text-center space-y-1">
<h3 className="font-semibold">No results found</h3>
<p className="text-sm text-muted-foreground">
Try adjusting your search
</p>
</div>
<Button variant="outline" size="sm">
Clear filters
</Button>
</CardContent>
</Card>
// Full width with constraints
<div className="container mx-auto px-4 max-w-7xl">
// Content-focused (prose)
<div className="container mx-auto px-4 max-w-3xl">
// Form-focused
<div className="container mx-auto px-4 max-w-2xl">
// Dashboard grid
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
// Content + sidebar
<div className="grid gap-6 lg:grid-cols-[1fr_300px]">
<main>{/* Content */}</main>
<aside>{/* Sidebar */}</aside>
</div>
// Two column split
<div className="grid gap-6 md:grid-cols-2">
<header className="border-b border-border">
<div className="container mx-auto flex h-16 items-center justify-between px-4">
<div className="flex items-center gap-6">
<Logo />
<nav className="hidden md:flex gap-6">
<a href="#" className="text-sm font-medium transition-colors hover:text-primary">
Dashboard
</a>
<a href="#" className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
Projects
</a>
</nav>
</div>
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm">
<Bell className="h-4 w-4" />
</Button>
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
</div>
</div>
</header>
// ✅ Good: Small, focused components
export function UserCard({ user }) {
return (
<Card>
<CardHeader>
<UserAvatar user={user} />
<UserDetails user={user} />
</CardHeader>
</Card>
)
}
// ❌ Avoid: Large monolithic components
export function DashboardPage() {
// 500 lines of JSX...
}
// ✅ Compose shadcn components
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Share</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
// Form state
const [formData, setFormData] = useState({ name: '', email: '' })
// Loading states
const [isLoading, setIsLoading] = useState(false)
// UI states
const [isOpen, setIsOpen] = useState(false)
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
className={errors.email ? "border-destructive" : ""}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email}</p>
)}
</div>
</form>
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Delete</Button>
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Error</Badge>
Before completing: