BelajarKoding Logobelajarkoding

Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.

Navigasi

  • Artikel
  • Cheat Sheets
  • Roadmap
  • Challenges
  • Pricing
  • Search

Produk Lain

  • JagoHermes
  • KelasClaude
  • KilatKoding
  • BelajarVibeCoding
  • JualanKoding

Support

  • Privacy Policy
  • Terms of Service
  • Email

© 2026 BelajarKoding. All rights reserved.

Galih PratamaBagian dari ekosistem Galih Pratama
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade
belajarkoding LogobyGalih Pratama
RoadmapArtikelCheat SheetsChallengesUpgrade

Daftar Isi

InstalasiInit shadcn/uicomponents.jsonAdd ComponentsCLI CommandsCommon ComponentsCore ComponentsButtonInput dan FormForm dengan React Hook Form + ZodDialogDropdown MenuTableCommand (Command Palette)Toast (Sonner)ThemingCSS VariablesDark ModeCustom VariantsUsing cn() HelperTips dan PatternsServer Components + Client ComponentsCombine ComponentsGlossary
shadcn/uiReactTailwindUI Components

shadcn/ui Cheat Sheet

Referensi cepat shadcn/ui component library. CLI, components, theming, dark mode, Radix primitives, dan integrasi dengan React. Perfect buat developer yang bangun UI dengan Tailwind.

TypeScript9 min read1.655 kata
Silakan login atau daftar untuk membaca cheat sheet ini.

#Instalasi

#Init shadcn/ui

bash
# Create Next.js project (kalau belum ada)
npx create-next-app@latest my-app --typescript --tailwind --eslint
 
# Init shadcn/ui
npx shadcn@latest init
 
# Jawab pertanyaan:
# - Style: Default (atau New York)
# - Base color: Slate (atau pilih)
# - CSS variables: Yes

Setelah init, struktur project:

text
components/
  ui/              # shadcn components (di-copy ke project kamu)
lib/
  utils.ts         # cn() helper untuk merge classes
globals.css        # CSS variables buat theming
components.json    # Konfigurasi shadcn

#components.json

json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

#Add Components

#CLI Commands

bash
# Add single component
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
 
# Add multiple components
npx shadcn@latest add button card dialog input label
 
# Add all components (hati-hati, banyak banget)
npx shadcn@latest add --all
 
# Overwrite existing component
npx shadcn@latest add button --overwrite
 
# Add without install dependencies
npx shadcn@latest add button --no-deps
 
# List available components
npx shadcn@latest add --list

#Common Components

bash
# Form components
npx shadcn@latest add button input label textarea
npx shadcn@latest add select checkbox radio-group switch
npx shadcn@latest add slider form
 
# Layout components
npx shadcn@latest add card separator scroll-area
npx shadcn@latest add tabs accordion collapsible
npx shadcn@latest add sheet dialog
 
# Feedback components
npx shadcn@latest add alert toast sonner
npx shadcn@latest add progress skeleton spinner
 
# Navigation
npx shadcn@latest add navigation-menu breadcrumb
npx shadcn@latest add pagination
 
# Overlay
npx shadcn@latest add popover hover-card tooltip
npx shadcn@latest add command context-menu
 
# Data display
npx shadcn@latest add table avatar badge
npx shadcn@latest add calendar carousel
 
# Charts (baru)
npx shadcn@latest add chart

#Core Components

#Button

tsx
import { Button } from "@/components/ui/button";
 
// Variants
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
 
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">🚀</Button>
 
// With icon
<Button>
  <Plus className="mr-2 h-4 w-4" />
  Add Item
</Button>
 
// Loading state
<Button disabled>
  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  Loading...
</Button>
 
// As link
<Button asChild>
  <a href="/dashboard">Go to Dashboard</a>
</Button>

#Input dan Form

tsx
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
 
// Basic input
<div className="grid gap-2">
  <Label htmlFor="email">Email</Label>
  <Input id="email" type="email" placeholder="hi@galihpratama.com" />
</div>
 
// Select
<Select>
  <SelectTrigger className="w-[180px]">
    <SelectValue placeholder="Pilih kategori" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="tech">Tech</SelectItem>
    <SelectItem value="design">Design</SelectItem>
    <SelectItem value="business">Business</SelectItem>
  </SelectContent>
</Select>
 
// Textarea
<Textarea
  placeholder="Tulis deskripsi..."
  className="min-h-[100px]"
/>

#Form dengan React Hook Form + Zod

tsx
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";
 
const formSchema = z.object({
  username: z.string().min(2, "Minimal 2 karakter"),
  email: z.string().email("Email tidak valid"),
});
 
function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { username: "", email: "" },
  });
 
  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
  }
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="galihpratama" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" placeholder="hi@example.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

#Dialog

tsx
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogFooter,
  DialogClose,
} from "@/components/ui/dialog";
 
function DeleteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Hapus</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Konfirmasi Hapus</DialogTitle>
          <DialogDescription>
            Kamu yakin mau hapus item ini? Tindakan ini tidak bisa dibatalkan.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Batal</Button>
          </DialogClose>
          <Button variant="destructive" onClick={handleDelete}>
            Hapus Permanen
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
 
// Controlled dialog
const [open, setOpen] = useState(false);
 
<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent>...</DialogContent>
</Dialog>

#Dropdown Menu

tsx
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { MoreHorizontal } from "lucide-react";
 
function ActionMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon">
          <MoreHorizontal className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuLabel>Actions</DropdownMenuLabel>
        <DropdownMenuItem onClick={() => edit()}>Edit</DropdownMenuItem>
        <DropdownMenuItem onClick={() => duplicate()}>Duplicate</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem
          className="text-red-600"
          onClick={() => del()}
        >
          Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

#Table

tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
 
function UserTable({ users }: { users: User[] }) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Name</TableHead>
          <TableHead>Email</TableHead>
          <TableHead className="text-right">Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {users.map((user) => (
          <TableRow key={user.id}>
            <TableCell className="font-medium">{user.name}</TableCell>
            <TableCell>{user.email}</TableCell>
            <TableCell className="text-right">
              <Button size="sm" variant="ghost">Edit</Button>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

#Command (Command Palette)

tsx
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command";
 
function CommandMenu() {
  const [open, setOpen] = useState(false);
 
  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 (
    <CommandDialog open={open} onOpenChange={setOpen}>
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <File className="mr-2 h-4 w-4" />
            <span>New File</span>
            <CommandShortcut>⌘N</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <Settings className="mr-2 h-4 w-4" />
            <span>Settings</span>
            <CommandShortcut>⌘,</CommandShortcut>
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Theme">
          <CommandItem onSelect={() => setTheme("light")}>
            <Sun className="mr-2 h-4 w-4" /> Light
          </CommandItem>
          <CommandItem onSelect={() => setTheme("dark")}>
            <Moon className="mr-2 h-4 w-4" /> Dark
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </CommandDialog>
  );
}

#Toast (Sonner)

tsx
import { toast } from "sonner";
 
// Basic toast
toast("Event has been created");
 
// Success
toast.success("Settings saved successfully!");
 
// Error
toast.error("Failed to save. Please try again.");
 
// Warning
toast.warning("This action cannot be undone.");
 
// Info
toast.info("New update available.");
 
// With action
toast("File deleted", {
  action: {
    label: "Undo",
    onClick: () => restoreFile(),
  },
});
 
// With description
toast.success("Order placed", {
  description: "Sunday, June 21, 2026 at 9:00 AM",
});
 
// Promise
toast.promise(apiCall(), {
  loading: "Loading...",
  success: "Data loaded!",
  error: "Failed to load.",
});
 
// Custom duration
toast("Quick message", { duration: 2000 });
 
// Dismiss
toast.dismiss(toastId);

#Theming

#CSS Variables

css
/* app/globals.css */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-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%;
  --input: 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%;
  --card: 222.2 84% 4.9%;
  --card-foreground: 210 40% 98%;
  /* ... dark variants */
}

#Dark Mode

tsx
// components/theme-provider.tsx
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
 
export function ThemeProvider({ children, ...props }) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
 
// components/mode-toggle.tsx
"use client";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
 
export function ModeToggle() {
  const { setTheme } = useTheme();
 
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

#Custom Variants

Edit component yang udah di-copy buat tambah variant.

tsx
// components/ui/button.tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center...",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        // Tambah custom variant
        success: "bg-green-600 text-white hover:bg-green-700",
        warning: "bg-yellow-500 text-white hover:bg-yellow-600",
      },
      size: {
        default: "h-10 px-4 py-2",
        // Tambah custom size
        xl: "h-14 px-8 text-lg",
      },
    },
  }
);
 
// Usage
<Button variant="success">Saved!</Button>
<Button size="xl">Big Button</Button>

#Using cn() Helper

tsx
import { cn } from "@/lib/utils";
 
// Merge Tailwind classes
<div className={cn("p-4 bg-white", isActive && "bg-blue-50 border-blue-200")}>
  Content
</div>
 
// Override default component styles
<Button className={cn("w-full", "mt-4")}>
  Full Width Button
</Button>

#Tips dan Patterns

#Server Components + Client Components

tsx
// shadcn/ui components adalah client components (pakai Radix)
// Di Next.js App Router, import di client component
 
// page.tsx (Server Component)
import { getUser } from "@/lib/actions";
import { ProfileCard } from "@/components/profile-card";
 
export default async function Page() {
  const user = await getUser();
  return <ProfileCard user={user} />;
}
 
// profile-card.tsx ("use client")
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
 
export function ProfileCard({ user }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{user.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <Button>Edit Profile</Button>
      </CardContent>
    </Card>
  );
}

#Combine Components

tsx
// Sidebar layout pattern
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Menu } from "lucide-react";
import { Sidebar } from "@/components/sidebar";
 
function MobileNav() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="ghost" size="icon" className="md:hidden">
          <Menu className="h-5 w-5" />
        </Button>
      </SheetTrigger>
      <SheetContent side="left" className="w-72">
        <Sidebar />
      </SheetContent>
    </Sheet>
  );
}

#Glossary

  • Radix UI: Set headless (unstyled) accessible components yang jadi foundation shadcn/ui.
  • Headless Component: Component yang handle logic dan accessibility, tapi tanpa styling. Kamu yang atur CSS.
  • cn(): Utility function buat merge Tailwind classes secara kondisional (clsx + tailwind-merge).
  • CSS Variables: Custom properties di :root yang ngasih value warna, spacing, dll. Bikin theming dinamis.
  • Variant (cva): Class Variance Authority. Library buat define multiple variants component secara type-safe.
  • Copy-paste (bukan npm): shadcn/ui components di-copy ke project kamu, bukan install sebagai dependency. Kamu punya full control kode-nya.
  • Form: shadcn/ui Form component adalah wrapper di atas React Hook Form yang integrate sama shadcn UI components.
  • RSC Compatible: Bisa dipakai di React Server Components (Next.js App Router). Client components di-mark dengan "use client".

Baca Cheat Sheet Lengkap

Login atau daftar akun gratis untuk membaca cheat sheet ini.

LoginDaftar Gratis
Share: