belajarkoding Platform belajar web development Indonesia. Artikel, cheat sheets, roadmap, dan code challenges untuk developer Indonesia.
© 2026 BelajarKoding. All rights reserved.
Bagian dari ekosistem Galih Pratama
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.
TypeScript 9 min read 1.655 kata
Silakan
login atau
daftar untuk membaca cheat sheet ini.
Baca Cheat Sheet Lengkap Login atau daftar akun gratis untuk membaca cheat sheet ini.
shadcn/ui Cheat Sheet - BelajarKoding | BelajarKoding
# Instalasi
# Init shadcn/ui
# 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:
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 {
"$schema" : "https://ui.shadcn.com/schema.json" ,
"style" : "default" ,
"rsc" : true ,
"tsx" : true ,
"tailwind" : {
"config" : "tailwind.config.ts" ,
# 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
# Form components
npx shadcn@latest add button input label textarea
npx shadcn@latest add select checkbox radio-group switch
npx shadcn@latest add slider
import { Button } from "@/components/ui/button" ;
// Variants
< Button variant = "default" >Default</ Button >
< Button variant = "secondary" >Secondary</
import { Input } from "@/components/ui/input" ;
import { Label } from "@/components/ui/label" ;
import { Textarea } from "@/components/ui/textarea" ;
import {
Select,
# Form dengan React Hook Form + Zodimport { useForm } from "react-hook-form" ;
import { zodResolver } from
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
DialogClose,
} from
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table" ;
function UserTable ({ users
# Command (Command Palette)import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
import { toast } from "sonner" ;
// Basic toast
toast ( "Event has been created" );
// Success
toast. success ( "Settings saved successfully!" );
// Error
/* app/globals.css */
:root {
--background : 0 0 % 100 % ;
--foreground : 222.2 84 % 4.9 % ;
// components/theme-provider.tsx
"use client" ;
import { ThemeProvider as NextThemesProvider } from "next-themes" ;
Edit component yang udah di-copy buat tambah variant.
// 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"
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
# Server Components + Client Components// 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
// 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" ;
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".
"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 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
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
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 >
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]"
/>
"@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 >
);
}
"@/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 >
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 >
);
}
}
:
{
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 >
);
}
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. 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);
--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.5 rem ;
}
.dark {
--background : 222.2 84 % 4.9 % ;
--foreground : 210 40 % 98 % ;
--card : 222.2 84 % 4.9 % ;
--card-foreground : 210 40 % 98 % ;
/* ... dark variants */
}
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 >
);
}
,
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 >
className
=
{
cn
(
"w-full"
,
"mt-4"
)}>
Full Width Button
</ Button >
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 >
);
}
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 >
);
}