Skip to content

shadcn/ui Reference: Install, Components, Forms, Data Table & Theming

shadcn/ui is not a component library — it’s a collection of copy-paste components built on Radix UI + Tailwind CSS. You own the code. Components live in your src/components/ui/ directory and you modify them directly. This is the key difference from installing a library: no version lock-in, no fighting component APIs, full control. The CLI installs components individually and wires up dependencies automatically.

1. Setup & CLI

Install, init, add components, and understand the copy-paste model
# New project (Next.js):
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npx shadcn@latest init

# init prompts:
# ✓ Which style? → New York (or Default — prefer New York, more refined)
# ✓ Which base color? → Slate (or Zinc/Stone/Gray/Neutral)
# ✓ Use CSS variables? → Yes (recommended — enables theming)

# components.json (created by init):
{
  "style": "new-york",
  "rsc": true,            # React Server Components
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"    # cn() helper lives here
  }
}

# Add individual components:
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add table
npx shadcn@latest add data-table   # includes @tanstack/react-table

# Components land in src/components/ui/button.tsx — EDIT THEM FREELY
# Update a component to latest shadcn version:
npx shadcn@latest add button --overwrite   # use with care — overwrites your edits

2. Core Components — Button, Dialog, Form

Button variants, Dialog with state, Form with react-hook-form + zod validation
// Button — variants and sizes:
import { Button } from "@/components/ui/button";

<Button>Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Trash2 className="h-4 w-4" /></Button>
<Button disabled>Loading...</Button>
<Button asChild><Link href="/about">About</Link></Button> {/* render as Link */}

// Dialog:
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";

function DeleteConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
  const [open, setOpen] = useState(false);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
        </DialogHeader>
        <p>This action cannot be undone.</p>
        <div className="flex justify-end gap-2">
          <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
          <Button variant="destructive" onClick={() => { onConfirm(); setOpen(false); }}>Delete</Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}

3. Form with react-hook-form + zod

shadcn Form component wires react-hook-form and zod together with accessible error messages
// npx shadcn@latest add form input select textarea
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

const schema = z.object({
  name:  z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email"),
});
type FormValues = z.infer<typeof schema>;

export function ProfileForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { name: "", email: "" },
  });

  function onSubmit(values: FormValues) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input placeholder="Alice Smith" {...field} />
              </FormControl>
              <FormMessage />  {/* shows zodError message */}
            </FormItem>
          )}
        />
        <Button type="submit" disabled={form.formState.isSubmitting}>
          {form.formState.isSubmitting ? "Saving..." : "Save"}
        </Button>
      </form>
    </Form>
  );
}

4. Data Table, Toast & Theming

Data table with @tanstack/react-table, Toaster notifications, and CSS variable theming
// Data Table (npx shadcn@latest add data-table):
import { useReactTable, getCoreRowModel, flexRender,
         getSortedRowModel, getPaginationRowModel } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";

const columns = [
  { accessorKey: "name",  header: "Name" },
  { accessorKey: "email", header: "Email" },
  { accessorKey: "role",  header: () => <div className="text-right">Role</div>,
    cell: ({ row }) => <Badge>{row.original.role}</Badge> },
];

function UsersTable({ data }: { data: User[] }) {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel() });
  return (
    <Table>
      <TableHeader>{table.getHeaderGroups().map(hg => (
        <TableRow key={hg.id}>{hg.headers.map(h => (
          <TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</TableHead>
        ))}</TableRow>
      ))}</TableHeader>
      <TableBody>{table.getRowModel().rows.map(row => (
        <TableRow key={row.id}>{row.getVisibleCells().map(cell => (
          <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
        ))}</TableRow>
      ))}</TableBody>
    </Table>
  );
}

// Toast (npx shadcn@latest add sonner):
import { toast } from "sonner";
toast.success("Saved successfully!");
toast.error("Something went wrong", { description: "Please try again." });
toast.promise(saveData(), { loading: "Saving...", success: "Saved!", error: "Error" });

// CSS variable theming (globals.css) — override to create a custom theme:
// :root { --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; }
// .dark { --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; }

5. cn() Utility, Custom Variants & Server Components

cn() class merging, cva() for variants, and RSC compatibility
// cn() — merge Tailwind classes without conflicts (lib/utils.ts):
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// Usage — conditional + merged classes:
<div className={cn("p-4 rounded-lg", isError && "bg-red-50 border-red-200", className)} />
// twMerge handles: cn("p-4", "p-8") → "p-8" (last wins, no duplicate)

// cva() — class variance authority for component variants (already used in Button):
import { cva, type VariantProps } from "class-variance-authority";

const alertVariants = cva("rounded-lg border p-4 flex gap-3", {
  variants: {
    variant: {
      default:     "bg-background text-foreground",
      destructive: "border-red-200 bg-red-50 text-red-700",
      success:     "border-green-200 bg-green-50 text-green-700",
    },
  },
  defaultVariants: { variant: "default" },
});

interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}

const Alert = ({ variant, className, ...props }: AlertProps) =>
  <div className={cn(alertVariants({ variant }), className)} {...props} />;

// Server Components — shadcn components are RSC-compatible by default.
// Exceptions: anything with state/hooks (Dialog, DropdownMenu, Toast) — add "use client".
// Pattern: Server Component wraps → passes data → Client Component handles interactions:
// page.tsx (Server): const users = await db.users.findMany()
// UsersClient.tsx (Client): "use client"; function UsersClient({ users }) { ... }

Track Node.js and frontend releases at ReleaseRun. Related: Next.js App Router Reference | Zod Reference | TypeScript Reference

🔍 Free tool: npm Package Health Checker — check shadcn/ui dependencies — Radix UI, Tailwind — for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com