shadcn/ui - Kompletny przewodnik po najpopularniejszej bibliotece komponentów React
Czym jest shadcn/ui i dlaczego zrewolucjonizował frontend development?
shadcn/ui to nie jest typowa biblioteka komponentów. To fundamentalnie inne podejście do budowania interfejsów użytkownika w React. Zamiast instalować paczkę npm i importować komponenty, kopiujesz kod źródłowy bezpośrednio do swojego projektu. Brzmi dziwnie? To właśnie ta filozofia sprawiła, że shadcn/ui stał się jednym z najpopularniejszych narzędzi w ekosystemie React.
Projekt został stworzony przez shadcn (prawdziwe imię: Shad) i szybko zyskał ogromną popularność w społeczności developerów. W 2023 roku stał się de facto standardem dla nowych projektów Next.js, a w 2024 roku jego popularność tylko wzrosła.
Dlaczego shadcn/ui jest tak popularne?
Pełna kontrola nad kodem
Gdy instalujesz tradycyjną bibliotekę UI jak Material UI czy Chakra UI, komponenty są ukryte w node_modules. Jeśli potrzebujesz zmodyfikować zachowanie przycisku, musisz używać skomplikowanych overrides lub wrapper components. Z shadcn/ui kod jest w twoim projekcie - możesz zmienić dosłownie wszystko.
Brak vendor lock-in
Ponieważ kod należy do ciebie, nie jesteś uzależniony od aktualizacji biblioteki. Możesz zamrozić wersję komponentu, zmodyfikować go pod swoje potrzeby lub całkowicie przepisać - bez obawy o breaking changes w następnej wersji.
Dostępność (Accessibility) od podstaw
Pod spodem shadcn/ui używa Radix UI - biblioteki primitives, która jest liderem w kwestii dostępności. Każdy komponent jest w pełni dostępny dla czytników ekranowych, obsługuje nawigację klawiaturą i spełnia standardy WCAG.
Nowoczesny stack technologiczny
shadcn/ui jest zbudowany na:
- Radix UI - headless primitives z pełną dostępnością
- Tailwind CSS - utility-first styling
- TypeScript - pełne typowanie
- Class Variance Authority (CVA) - zarządzanie wariantami
Instalacja i konfiguracja
Wymagania wstępne
Przed instalacją shadcn/ui potrzebujesz projektu z:
- React 18+
- Tailwind CSS 3.4+
- TypeScript (opcjonalnie, ale zalecane)
Inicjalizacja w Next.js
npx shadcn@latest initPodczas inicjalizacji zostaniesz zapytany o:
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › app/globals.css
Would you like to use CSS variables for colors? › yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utilsStruktura po inicjalizacji
Po inicjalizacji twój projekt będzie miał następującą strukturę:
project/
├── components/
│ └── ui/ # Tu trafią komponenty
├── lib/
│ └── utils.ts # Funkcja cn() do łączenia klas
├── components.json # Konfiguracja shadcn/ui
└── tailwind.config.jsPlik utils.ts
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Funkcja cn() to serce stylowania w shadcn/ui. Łączy klasy CSS inteligentnie, rozwiązując konflikty Tailwind (np. bg-red-500 i bg-blue-500 - wygrywa ostatnia).
Dodawanie komponentów
CLI - zalecana metoda
# Pojedynczy komponent
npx shadcn@latest add button
# Wiele komponentów naraz
npx shadcn@latest add button card dialog input textarea
# Wszystkie dostępne komponenty
npx shadcn@latest add --allManualne kopiowanie
Możesz też skopiować kod bezpośrednio z dokumentacji. Każdy komponent ma przycisk "Copy" z pełnym kodem źródłowym.
Przegląd najważniejszych komponentów
Button - podstawowy przycisk
Button to najprostszy, ale jednocześnie najczęściej używany komponent:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }Użycie w praktyce:
import { Button } from "@/components/ui/button"
import { Mail, Loader2 } from "lucide-react"
export function ButtonExamples() {
return (
<div className="flex flex-col gap-4">
{/* Podstawowe warianty */}
<div className="flex gap-2">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
{/* Rozmiary */}
<div className="flex items-center gap-2">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Mail className="h-4 w-4" /></Button>
</div>
{/* Ze stanem loading */}
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
{/* Jako link */}
<Button asChild>
<a href="/login">Login</a>
</Button>
</div>
)
}Form - kompleksowe formularze z walidacją
shadcn/ui integruje się znakomicie z React Hook Form i Zod:
npx shadcn@latest add form input label
npm install @hookform/resolvers zod"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
password: z.string().min(8, {
message: "Password must be at least 8 characters.",
}).regex(/[A-Z]/, {
message: "Password must contain at least one uppercase letter.",
}).regex(/[0-9]/, {
message: "Password must contain at least one number.",
}),
})
type FormValues = z.infer<typeof formSchema>
export function RegistrationForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
password: "",
},
})
async function onSubmit(values: FormValues) {
try {
// Wyślij dane do API
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("Registration failed")
toast({
title: "Account created!",
description: "You can now log in with your credentials.",
})
} catch (error) {
toast({
title: "Error",
description: "Something went wrong. Please try again.",
variant: "destructive",
})
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Min. 8 characters, one uppercase, one number.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Create account
</Button>
</form>
</Form>
)
}Dialog - modalne okna dialogowe
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function EditProfileDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit Profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" defaultValue="John Doe" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input id="username" defaultValue="@johndoe" className="col-span-3" />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
Cancel
</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Data Table - zaawansowane tabele z TanStack Table
npx shadcn@latest add table
npm install @tanstack/react-table"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
getSortedRowModel,
SortingState,
getFilteredRowModel,
ColumnFiltersState,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useState } from "react"
import { ArrowUpDown, MoreHorizontal } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
// Definicja typu danych
type User = {
id: string
email: string
name: string
status: "active" | "inactive" | "pending"
createdAt: Date
}
// Definicja kolumn
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string
return (
<span className={`px-2 py-1 rounded-full text-xs ${
status === "active" ? "bg-green-100 text-green-800" :
status === "pending" ? "bg-yellow-100 text-yellow-800" :
"bg-gray-100 text-gray-800"
}`}>
{status}
</span>
)
},
},
{
id: "actions",
cell: ({ row }) => {
const user = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(user.id)}>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
export function UsersTable({ data }: { data: User[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
state: {
sorting,
columnFilters,
},
})
return (
<div className="space-y-4">
<Input
placeholder="Filter by email..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
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>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}Theming i customizacja
System kolorów z CSS Variables
shadcn/ui używa CSS variables, co umożliwia łatwe theming:
/* app/globals.css */
@layer base {
: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%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}Tworzenie własnych wariantów
// Rozszerz istniejący komponent
const buttonVariants = cva(
"inline-flex items-center justify-center...",
{
variants: {
variant: {
// Istniejące warianty...
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// Twoje własne warianty
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600",
success: "bg-green-500 text-white hover:bg-green-600",
warning: "bg-yellow-500 text-black hover:bg-yellow-600",
},
size: {
// Własne rozmiary
xs: "h-7 rounded px-2 text-xs",
xl: "h-14 rounded-lg px-10 text-lg",
},
},
}
)Porównanie z innymi bibliotekami UI
| Cecha | shadcn/ui | Material UI | Chakra UI | Ant Design |
|---|---|---|---|---|
| Ownership kodu | Pełna | Brak | Brak | Brak |
| Bundle size | Tylko używane | ~300KB | ~150KB | ~400KB |
| Customizacja | Bezpośrednia | Theme/Override | Props | Theme |
| Accessibility | Radix (AAA) | Dobra | Bardzo dobra | Dobra |
| TypeScript | Natywny | Dobry | Dobry | Dobry |
| Learning curve | Niska | Średnia | Niska | Wysoka |
| Design system | Tailwind | Material | Własny | Ant |
Najlepsze praktyki
1. Organizacja komponentów
components/
├── ui/ # Komponenty shadcn/ui (nie modyfikuj nazw)
│ ├── button.tsx
│ ├── card.tsx
│ └── dialog.tsx
├── forms/ # Komponenty formularzy
│ ├── login-form.tsx
│ └── register-form.tsx
└── shared/ # Własne komponenty używające shadcn/ui
├── user-avatar.tsx
└── navigation.tsx2. Nie modyfikuj oryginalnych plików
Zamiast modyfikować button.tsx, stwórz wrapper:
// components/shared/primary-button.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { cn } from "@/lib/utils"
export function PrimaryButton({ className, ...props }: ButtonProps) {
return (
<Button
className={cn("bg-brand-500 hover:bg-brand-600", className)}
{...props}
/>
)
}3. Używaj composable patterns
// Zamiast jednego dużego komponentu
function UserCard({ user, showActions, showBadge, variant }) {
// Skomplikowana logika warunkowa
}
// Preferuj composition
function UserCard({ children }) {
return <Card>{children}</Card>
}
function UserCardHeader({ user }) { /* ... */ }
function UserCardActions({ onEdit, onDelete }) { /* ... */ }
function UserCardBadge({ status }) { /* ... */ }
// Użycie
<UserCard>
<UserCardHeader user={user} />
<UserCardBadge status={user.status} />
<UserCardActions onEdit={handleEdit} onDelete={handleDelete} />
</UserCard>Często zadawane pytania (FAQ)
Czy shadcn/ui jest darmowe?
Tak, shadcn/ui jest w pełni darmowe i open-source na licencji MIT. Możesz używać go w projektach komercyjnych bez żadnych opłat.
Czy mogę używać shadcn/ui bez Tailwind CSS?
Nie, shadcn/ui wymaga Tailwind CSS do stylowania. Komponenty są zbudowane z klasami Tailwind i nie działają bez niego.
Jak aktualizować komponenty?
Ponieważ kod jest w twoim projekcie, nie ma automatycznych aktualizacji. Możesz:
- Ręcznie skopiować nową wersję z dokumentacji
- Użyć
npx shadcn@latest add button --overwrite(uwaga: nadpisze twoje zmiany)
Czy shadcn/ui działa z Next.js App Router?
Tak, shadcn/ui jest w pełni kompatybilne z Next.js App Router. Komponenty klienckie używają dyrektywy "use client".
Jak dodać dark mode?
shadcn/ui wspiera dark mode przez CSS variables. Użyj next-themes:
npm install next-themes// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}Podsumowanie
shadcn/ui to rewolucyjne podejście do komponentów UI w React. Zamiast być zależnym od zewnętrznej biblioteki, masz pełną kontrolę nad kodem. To idealne rozwiązanie dla projektów, które wymagają wysokiego poziomu customizacji, dostępności i nowoczesnego designu.
Główne zalety:
- Pełna kontrola nad kodem
- Doskonała dostępność dzięki Radix UI
- Nowoczesny stack (Tailwind, TypeScript)
- Brak vendor lock-in
- Aktywna społeczność i dokumentacja
Jeśli zaczynasz nowy projekt React lub Next.js, shadcn/ui to zdecydowanie warte rozważenia jako fundament twojego design systemu.
shadcn/ui - Complete Guide to the Most Popular React Component Collection
What is shadcn/ui and Why Did It Revolutionize Frontend Development?
shadcn/ui is not a typical component library. It's a fundamentally different approach to building user interfaces in React. Instead of installing an npm package and importing components, you copy the source code directly into your project. Sounds strange? It's precisely this philosophy that made shadcn/ui one of the most popular tools in the React ecosystem.
The project was created by shadcn (real name: Shad) and quickly gained enormous popularity in the developer community. In 2023, it became the de facto standard for new Next.js projects, and in 2024, its popularity only grew.
Why is shadcn/ui So Popular?
Full Control Over Code
When you install a traditional UI library like Material UI or Chakra UI, components are hidden in node_modules. If you need to modify a button's behavior, you have to use complex overrides or wrapper components. With shadcn/ui, the code is in your project - you can change literally everything.
No Vendor Lock-in
Because the code belongs to you, you're not dependent on library updates. You can freeze a component version, modify it to your needs, or completely rewrite it - without fear of breaking changes in the next version.
Accessibility from the Ground Up
Under the hood, shadcn/ui uses Radix UI - a primitives library that is a leader in accessibility. Every component is fully accessible to screen readers, supports keyboard navigation, and meets WCAG standards.
Modern Technology Stack
shadcn/ui is built on:
- Radix UI - headless primitives with full accessibility
- Tailwind CSS - utility-first styling
- TypeScript - full typing
- Class Variance Authority (CVA) - variant management
Installation and Configuration
Prerequisites
Before installing shadcn/ui you need a project with:
- React 18+
- Tailwind CSS 3.4+
- TypeScript (optional, but recommended)
Initialization in Next.js
npx shadcn@latest initDuring initialization you'll be asked about:
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › app/globals.css
Would you like to use CSS variables for colors? › yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utilsStructure After Initialization
After initialization your project will have the following structure:
project/
├── components/
│ └── ui/ # Components will go here
├── lib/
│ └── utils.ts # cn() function for combining classes
├── components.json # shadcn/ui configuration
└── tailwind.config.jsThe utils.ts File
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}The cn() function is the heart of styling in shadcn/ui. It combines CSS classes intelligently, resolving Tailwind conflicts (e.g., bg-red-500 and bg-blue-500 - the last one wins).
Adding Components
CLI - Recommended Method
# Single component
npx shadcn@latest add button
# Multiple components at once
npx shadcn@latest add button card dialog input textarea
# All available components
npx shadcn@latest add --allManual Copying
You can also copy code directly from the documentation. Each component has a "Copy" button with full source code.
Overview of Key Components
Button - Basic Button
Button is the simplest yet most frequently used component:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }Practical usage:
import { Button } from "@/components/ui/button"
import { Mail, Loader2 } from "lucide-react"
export function ButtonExamples() {
return (
<div className="flex flex-col gap-4">
{/* Basic variants */}
<div className="flex gap-2">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
{/* Sizes */}
<div className="flex items-center gap-2">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Mail className="h-4 w-4" /></Button>
</div>
{/* With loading state */}
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
{/* As link */}
<Button asChild>
<a href="/login">Login</a>
</Button>
</div>
)
}Form - Complex Forms with Validation
shadcn/ui integrates excellently with React Hook Form and Zod:
npx shadcn@latest add form input label
npm install @hookform/resolvers zod"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
password: z.string().min(8, {
message: "Password must be at least 8 characters.",
}).regex(/[A-Z]/, {
message: "Password must contain at least one uppercase letter.",
}).regex(/[0-9]/, {
message: "Password must contain at least one number.",
}),
})
type FormValues = z.infer<typeof formSchema>
export function RegistrationForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
password: "",
},
})
async function onSubmit(values: FormValues) {
try {
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
})
if (!response.ok) throw new Error("Registration failed")
toast({
title: "Account created!",
description: "You can now log in with your credentials.",
})
} catch (error) {
toast({
title: "Error",
description: "Something went wrong. Please try again.",
variant: "destructive",
})
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Min. 8 characters, one uppercase, one number.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Create account
</Button>
</form>
</Form>
)
}Dialog - Modal Dialogs
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function EditProfileDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit Profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" defaultValue="John Doe" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input id="username" defaultValue="@johndoe" className="col-span-3" />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
Cancel
</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Theming and Customization
Color System with CSS Variables
shadcn/ui uses CSS variables, enabling easy theming:
/* app/globals.css */
@layer base {
: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%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}Creating Custom Variants
// Extend existing component
const buttonVariants = cva(
"inline-flex items-center justify-center...",
{
variants: {
variant: {
// Existing variants...
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// Your custom variants
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600",
success: "bg-green-500 text-white hover:bg-green-600",
warning: "bg-yellow-500 text-black hover:bg-yellow-600",
},
size: {
// Custom sizes
xs: "h-7 rounded px-2 text-xs",
xl: "h-14 rounded-lg px-10 text-lg",
},
},
}
)Comparison with Other UI Libraries
| Feature | shadcn/ui | Material UI | Chakra UI | Ant Design |
|---|---|---|---|---|
| Code ownership | Full | None | None | None |
| Bundle size | Only used | ~300KB | ~150KB | ~400KB |
| Customization | Direct | Theme/Override | Props | Theme |
| Accessibility | Radix (AAA) | Good | Very good | Good |
| TypeScript | Native | Good | Good | Good |
| Learning curve | Low | Medium | Low | High |
| Design system | Tailwind | Material | Custom | Ant |
Best Practices
1. Component Organization
components/
├── ui/ # shadcn/ui components (don't modify names)
│ ├── button.tsx
│ ├── card.tsx
│ └── dialog.tsx
├── forms/ # Form components
│ ├── login-form.tsx
│ └── register-form.tsx
└── shared/ # Custom components using shadcn/ui
├── user-avatar.tsx
└── navigation.tsx2. Don't Modify Original Files
Instead of modifying button.tsx, create a wrapper:
// components/shared/primary-button.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { cn } from "@/lib/utils"
export function PrimaryButton({ className, ...props }: ButtonProps) {
return (
<Button
className={cn("bg-brand-500 hover:bg-brand-600", className)}
{...props}
/>
)
}3. Use Composable Patterns
// Instead of one large component
function UserCard({ user, showActions, showBadge, variant }) {
// Complex conditional logic
}
// Prefer composition
function UserCard({ children }) {
return <Card>{children}</Card>
}
function UserCardHeader({ user }) { /* ... */ }
function UserCardActions({ onEdit, onDelete }) { /* ... */ }
function UserCardBadge({ status }) { /* ... */ }
// Usage
<UserCard>
<UserCardHeader user={user} />
<UserCardBadge status={user.status} />
<UserCardActions onEdit={handleEdit} onDelete={handleDelete} />
</UserCard>Frequently Asked Questions (FAQ)
Is shadcn/ui free?
Yes, shadcn/ui is fully free and open-source under the MIT license. You can use it in commercial projects without any fees.
Can I use shadcn/ui without Tailwind CSS?
No, shadcn/ui requires Tailwind CSS for styling. Components are built with Tailwind classes and don't work without it.
How do I update components?
Since the code is in your project, there are no automatic updates. You can:
- Manually copy the new version from the documentation
- Use
npx shadcn@latest add button --overwrite(warning: will overwrite your changes)
Does shadcn/ui work with Next.js App Router?
Yes, shadcn/ui is fully compatible with Next.js App Router. Client components use the "use client" directive.
How do I add dark mode?
shadcn/ui supports dark mode through CSS variables. Use next-themes:
npm install next-themes// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}Summary
shadcn/ui is a revolutionary approach to UI components in React. Instead of being dependent on an external library, you have full control over the code. It's the ideal solution for projects that require a high level of customization, accessibility, and modern design.
Main advantages:
- Full code control
- Excellent accessibility thanks to Radix UI
- Modern stack (Tailwind, TypeScript)
- No vendor lock-in
- Active community and documentation
If you're starting a new React or Next.js project, shadcn/ui is definitely worth considering as the foundation of your design system.