shadcn/ui is a set of React components you copy into your project with its CLI rather than install as a dependency. They are built on Radix primitives and styled with Tailwind. The value comes from combining them: below are eight everyday blocks, such as a sign-in card, pricing switch and delete confirmation, each live with code.
| Part | Comes from |
|---|---|
| Behaviour and accessibility | Radix primitives (focus, keyboard, ARIA) |
| Styling | Tailwind classes and CSS variables in your project |
| Source code | Copied into components/ui; you can edit it |
Updated
The previews run on this page using our own copies of the same components, themed with our colours, so buttons and corners look a little different from the stock theme. The code is plain shadcn/ui and works after the add command shown first.
Building a form or a data table? See shadcn form and table.
| Block | Components |
|---|---|
| Sign-in card | card, input, label, button |
| Stats row | card, badge |
| Pricing card | card, switch, label, button |
| Settings list | card, label, switch |
| Account tabs | tabs |
| Confirm delete | dialog, button |
| Empty state | card, button |
| Team list | avatar, badge |
shadcn/ui is not a package you import from npm. The CLI copies each component's source into your project, so you own and can edit it.
npx shadcn@latest init
npx shadcn@latest add button card input label switch tabs dialog badge avatarCard, Label, Input and Button. Every input has a visible label tied to it with htmlFor; the secondary link sits beside the label it relates to.
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function SignInCard() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Sign in</CardTitle>
<CardDescription>Use the email you booked with.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" autoComplete="email" placeholder="[email protected]" />
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<a href="/forgot" className="text-sm underline">Forgot it?</a>
</div>
<Input id="password" type="password" autoComplete="current-password" />
</div>
</CardContent>
<CardFooter><Button className="w-full">Sign in</Button></CardFooter>
</Card>
);
}Three cards in a responsive grid. tabular-nums keeps the figures from jumping when they change.
const stats = [
{ label: "Bookings this week", value: "48", change: "+12%" },
{ label: "New customers", value: "17", change: "+4" },
{ label: "No-shows", value: "2", change: "-3" },
];
<div className="grid gap-4 sm:grid-cols-3">
{stats.map((s) => (
<Card key={s.label}>
<CardHeader className="pb-2">
<CardDescription>{s.label}</CardDescription>
<CardTitle className="text-3xl tabular-nums">{s.value}</CardTitle>
</CardHeader>
<CardContent>
<Badge variant="secondary">{s.change}</Badge>
</CardContent>
</Card>
))}
</div>A Switch flips monthly and yearly prices. The label is clickable and the price change is announced because it sits in an aria-live region.
const [yearly, setYearly] = useState(false);
<Card className="max-w-xs">
<CardHeader>
<div className="flex items-center gap-2">
<Switch id="billing" checked={yearly} onCheckedChange={setYearly} />
<Label htmlFor="billing">Pay yearly</Label>
</div>
<CardTitle>Studio</CardTitle>
<p aria-live="polite" className="text-3xl font-semibold">
{yearly ? "$190" : "$19"}<span className="text-sm font-normal">/{yearly ? "year" : "month"}</span>
</p>
</CardHeader>
<CardContent>
<ul className="space-y-1 text-sm">
<li>Online booking</li><li>Reminders by email</li><li>Your own domain</li>
</ul>
</CardContent>
<CardFooter><Button className="w-full">Choose Studio</Button></CardFooter>
</Card>Each row pairs a Label and description with a Switch. Using Label htmlFor makes the whole text clickable, not just the small toggle.
const prefs = [
{ id: "reminders", title: "Booking reminders", text: "Email customers a day before." },
{ id: "reviews", title: "Review requests", text: "Ask for a review after each visit." },
];
<Card>
<CardHeader><CardTitle>Notifications</CardTitle></CardHeader>
<CardContent className="divide-y">
{prefs.map((p) => (
<div key={p.id} className="flex items-center justify-between gap-4 py-3">
<div>
<Label htmlFor={p.id}>{p.title}</Label>
<p className="text-sm text-muted-foreground">{p.text}</p>
</div>
<Switch id={p.id} defaultChecked />
</div>
))}
</CardContent>
</Card>shadcn Tabs are built on Radix, so arrow keys, Home and End already work and the ARIA roles are set for you.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
<Tabs defaultValue="profile" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="profile">Your name, photo and bio.</TabsContent>
<TabsContent value="billing">Your plan and invoices.</TabsContent>
</Tabs>Dialog handles focus trapping, Escape and returning focus to the trigger. Make the destructive button say exactly what will happen.
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter,
DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
<Dialog>
<DialogTrigger asChild><Button variant="destructive">Delete page</Button></DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete "About us"?</DialogTitle>
<DialogDescription>This removes the page and its menu link. You cannot undo it.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild><Button variant="outline">Keep page</Button></DialogClose>
<Button variant="destructive" onClick={deletePage}>Delete page</Button>
</DialogFooter>
</DialogContent>
</Dialog>A friendly empty state says what will appear here and offers the one action that fills it.
<Card className="border-dashed text-center">
<CardHeader>
<CardTitle>No bookings yet</CardTitle>
<CardDescription>Share your booking link and new bookings will show up here.</CardDescription>
</CardHeader>
<CardFooter className="justify-center gap-2">
<Button>Copy booking link</Button>
<Button variant="ghost">Add one by hand</Button>
</CardFooter>
</Card>Avatar shows the photo and falls back to initials when it is missing or still loading. The image needs alt text naming the person.
const team = [
{ name: "Maya Chen", role: "Owner", img: "/team/maya.jpg" },
{ name: "Tom Reid", role: "Stylist", img: "" },
];
<ul className="space-y-3">
{team.map((m) => (
<li key={m.name} className="flex items-center gap-3">
<Avatar>
<AvatarImage src={m.img} alt={m.name} />
<AvatarFallback>{m.name.split(" ").map((w) => w[0]).join("")}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{m.name}</p>
<p className="text-sm text-muted-foreground">{m.role}</p>
</div>
<Badge variant="outline" className="ml-auto">{m.role}</Badge>
</li>
))}
</ul>
Forms and data tables are the two places shadcn/ui hands most of the work to other libraries: React Hook Form with Zod for forms, and TanStack Table for tables. Our shadcn form and table guide covers both with live demos.
Mixing Radix packages from a different version than the one the copied component expects, which causes odd type errors. Add components with the CLI so it installs matching dependencies.
Overriding styles with long className strings on every usage. Add a variant to the component once instead.
Yes. shadcn/ui is open source under the MIT licence, so you can use and change the components in commercial projects.
Not in the usual sense. There is no package of components to update; the CLI copies the source into your project and you own it from then on. That makes changes easy but means updates are manual.
Yes. It works in any React setup with Tailwind, including Vite, React Router and Astro with React. The CLI asks which framework you use during init.
The official site has a set of blocks, and many community templates build full pages from them. For a quick start, compose sections like the ones here into a page, or use a website builder for the marketing site and keep shadcn/ui for the app.
Related: shadcn form and table, React Hook Form examples, UI components, React icons, All free tools.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.