shadcn/ui component usage, customization, theming, and best practices. Use when building UI with shadcn/ui components.
Install shadcn/ui for Next.js:
npx shadcn-ui@latest init -d
This initializes shadcn/ui with Tailwind CSS configuration.
Install individual components:
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add form
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
Button:
import { Button } from '@/components/ui/button'
<Button>Click me</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
Card:
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content here</CardContent>
</Card>
Input:
import { Input } from '@/components/ui/input'
<Input placeholder="Enter text" type="email" />
Form (with React Hook Form):
import { useForm } from 'react-hook-form'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form'
function MyForm() {
const form = useForm({
defaultValues: { email: '' },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...field} />
</FormControl>
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Dialog:
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { useState } from 'react'
function MyDialog() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<Button onClick={() => setOpen(true)}>Open</Button>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
</DialogHeader>
Content
</DialogContent>
</Dialog>
)
}
Dropdown Menu:
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Option 1</DropdownMenuItem>
<DropdownMenuItem>Option 2</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Using cn() for class merging:
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
customClass?: string
}
function CustomButton({ className, ...props }: CustomButtonProps) {
return <Button className={cn('custom-style', className)} {...props} />
}
Extending components:
// Create a variant wrapper
interface PrimaryButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
function PrimaryButton(props: PrimaryButtonProps) {
return <Button variant="default" size="lg" {...props} />
}
Custom theme colors in globals.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.6%;
--primary: 0 72.2% 50.6%;
--primary-foreground: 0 0% 100%;
/* ... other colors ... */
}
}
shadcn/ui components are accessible by default:
Always use semantic HTML:
// Good: Using Form component
<FormField ... />
// Bad: Manual accessibility
<input aria-label="email" />