본문으로 건너뛰기
Previous
Next
shadcn/ui Complete Guide | Radix UI· Tailwind

shadcn/ui Complete Guide | Radix UI· Tailwind

shadcn/ui Complete Guide | Radix UI· Tailwind

이 글의 핵심

shadcn/ui is a collection of reusable components built with Radix UI and Tailwind CSS. Unlike traditional libraries, you copy components directly into your project for full control and customization.

Introduction

shadcn/ui is not a traditional component library. Instead of installing an npm package, you copy components directly into your project. This approach gives you complete control over the code while providing beautifully designed, accessible components.

Created by shadcn (Shadcn Mohamed), this system has rapidly gained adoption in the React ecosystem. Companies building products with Next.js, Remix, and Vite frequently choose shadcn/ui for its flexibility and developer experience. Unlike libraries that require you to work within their constraints, shadcn/ui gives you the source code ??you decide how it works.

Why shadcn/ui?

Traditional Libraries (Material-UI, Ant Design, Chakra):

  • Install entire library (100-500KB+), use only 10-20%
  • Customization requires theme overrides, CSS specificity battles
  • Upgrades can break your customizations
  • Abstracted away ??can’t easily see or modify internals
  • Hard to match your exact design system

shadcn/ui Approach:

  • Copy only the 5-10 components you actually use (~20-40KB total)
  • Own the code ??modify any line directly in your codebase
  • No breaking changes from library updates (you control updates)
  • Built on Radix UI (battle-tested accessibility primitives)
  • Styled with Tailwind (utility-first, easy to customize)
  • Perfect for design systems ??start with shadcn, adapt to your brand

Real-World Adoption

Projects using shadcn/ui include:

  • v0.dev (Vercel’s AI design tool)
  • Cal.com (open-source scheduling platform)
  • Hundreds of SaaS dashboards, admin panels, and internal tools
  • Developer portfolios and personal projects

The “copy-paste” model means you can inspect production sites using shadcn/ui, understand exactly how components work, and adapt patterns to your needs.

1. Installation & Setup

Prerequisites

# Requires Node.js 18+
node --version

# Create Next.js project (recommended)
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app

Initialize shadcn/ui

npx shadcn-ui@latest init

Configuration prompts:

??Which style would you like to use? ??Default
??Which color would you like to use as base color? ??Slate
??Would you like to use CSS variables for colors? ??yes

Generated Files

my-app/
?��??� components/
??  ?��??� ui/           # Components go here
?��??� lib/
??  ?��??� utils.ts      # Utility functions (cn helper)
?��??� tailwind.config.ts
?��??� components.json   # shadcn/ui config

components.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
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

2. Adding Components

Button Component

npx shadcn-ui@latest add button

This copies the Button component to components/ui/button.tsx.

Usage:

// app/page.tsx
import { Button } from '@/components/ui/button';

export default function Home() {
  return (
    <div className="p-8 space-x-4">
      <Button>Default</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
      
      <Button size="sm">Small</Button>
      <Button size="lg">Large</Button>
      <Button size="icon">??</Button>
    </div>
  );
}

Card Component

npx shadcn-ui@latest add card
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';

export default function ProductCard() {
  return (
    <Card className="w-[350px]">
      <CardHeader>
        <CardTitle>Product Name</CardTitle>
        <CardDescription>Product description goes here</CardDescription>
      </CardHeader>
      <CardContent>
        <p>Detailed product information...</p>
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">Cancel</Button>
        <Button>Buy Now</Button>
      </CardFooter>
    </Card>
  );
}

Form Components

npx shadcn-ui@latest add input label
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

export default function LoginForm() {
  return (
    <form className="space-y-4">
      <div>
        <Label htmlFor="email">Email</Label>
        <Input id="email" type="email" placeholder="[email protected]" />
      </div>
      
      <div>
        <Label htmlFor="password">Password</Label>
        <Input id="password" type="password" />
      </div>
      
      <Button type="submit" className="w-full">
        Sign In
      </Button>
    </form>
  );
}

3. Form Integration with React Hook Form

npx shadcn-ui@latest add form
npm install react-hook-form @hookform/resolvers zod
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as 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';

const formSchema = z.object({
  username: z.string().min(2, 'Username must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
});

export default 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-8">
        <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="[email protected]" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit">Update profile</Button>
      </form>
    </Form>
  );
}

4. Dialog & Modal

npx shadcn-ui@latest add dialog
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

export default function UserDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>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="Pedro Duarte" 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="@peduarte" className="col-span-3" />
          </div>
        </div>
        
        <DialogFooter>
          <Button type="submit">Save changes</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

5. Data Tables

npx shadcn-ui@latest add table
npm install @tanstack/react-table
'use client';

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

const invoices = [
  { id: 'INV001', status: 'Paid', amount: '$250.00' },
  { id: 'INV002', status: 'Pending', amount: '$150.00' },
  { id: 'INV003', status: 'Unpaid', amount: '$350.00' },
];

export default function InvoiceTable() {
  return (
    <Table>
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[100px]">Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead className="text-right">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell className="font-medium">{invoice.id}</TableCell>
            <TableCell>{invoice.status}</TableCell>
            <TableCell className="text-right">{invoice.amount}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

6. Toast Notifications

npx shadcn-ui@latest add toast
'use client';

import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';

export default function ToastDemo() {
  const { toast } = useToast();

  return (
    <Button
      onClick={() => {
        toast({
          title: 'Scheduled: Catch up',
          description: 'Friday, February 10, 2023 at 5:57 PM. shadcn/ui Complete Guide???�???�전??가?�드?�니?? ?�전 ?�제?� ?�께 ?�심 개념부??고급 ?�용까�? ?�룹?�다.',
        });
      }}
    >
      Show Toast
    </Button>
  );
}

Toaster Component:

// app/layout.tsx
import { Toaster } from '@/components/ui/toaster';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

7. Customization

Modify Component Styles

Since you own the code, you can modify components directly:

// components/ui/button.tsx
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        
        // Add your custom variant
        custom: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:opacity-90',
      },
    },
  }
);

Theme Colors

Edit globals.css:

@layer base {
  :root {
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    /* Add custom colors */
    --custom: 280 100% 70%;
  }

  .dark {
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }
}

8. Dark Mode

npm install next-themes
// app/providers.tsx
'use client';

import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}

Theme Toggle:

'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

9. Server Components

shadcn/ui works with Next.js Server Components:

// app/users/page.tsx (Server Component)
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

async function getUsers() {
  const res = await fetch('https://api.example.com/users');
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {users.map((user: any) => (
        <Card key={user.id}>
          <CardHeader>
            <CardTitle>{user.name}</CardTitle>
          </CardHeader>
          <CardContent>
            <p>{user.email}</p>
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

10. Best Practices

1. Use the cn Utility

import { cn } from '@/lib/utils';

<Button className={cn('w-full', isLoading && 'opacity-50')} />

2. Create Composed Components

// components/search-input.tsx
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';

export function SearchInput() {
  return (
    <div className="relative">
      <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
      <Input placeholder="Search..." className="pl-8" />
    </div>
  );
}

3. Organize Components

components/
?��??� ui/              # shadcn/ui components
?��??� forms/           # Form-specific components
?��??� layouts/         # Layout components
?��??� shared/          # Shared custom components

4. Type Safety

import { type ButtonProps } from '@/components/ui/button';

interface CustomButtonProps extends ButtonProps {
  icon?: React.ReactNode;
}

export function CustomButton({ icon, children, ...props }: CustomButtonProps) {
  return (
    <Button {...props}>
      {icon && <span className="mr-2">{icon}</span>}
      {children}
    </Button>
  );
}

11. Common Patterns

Loading States

import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';

export function LoadingButton() {
  const [isLoading, setIsLoading] = useState(false);

  return (
    <Button disabled={isLoading} onClick={() => setIsLoading(true)}>
      {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
      {isLoading ? 'Loading...' : 'Submit'}
    </Button>
  );
}

Confirmation Dialog

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from '@/components/ui/alert-dialog';

export function DeleteConfirmation() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="destructive">Delete</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
          <AlertDialogDescription>
            This action cannot be undone.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction onClick={() => console.log('Deleted')}>
            Continue
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

Summary

shadcn/ui revolutionizes how we build UIs in React:

  • Copy-paste components you own and control
  • Built on Radix UI for perfect accessibility
  • Styled with Tailwind for easy customization
  • TypeScript first with excellent type safety
  • Works with Server Components in Next.js

Key Takeaways:

  1. You own the code - modify freely
  2. Only copy what you need - smaller bundles
  3. Built on solid foundations (Radix + Tailwind)
  4. Perfect for design systems
  5. Excellent developer experience

Next Steps:

  • Explore [Tailwind CSS](/en/blog/tailwind-css-complete-guide/ for advanced styling
  • Learn [React Hook Form](/en/blog/react-hook-form-complete-guide/ for forms
  • Check [Next.js 15](/en/blog/nextjs-15-complete-guide/ for full-stack apps

Resources:


?�주 묻는 질문 (FAQ)

Q. ???�용???�무?�서 ?�제 ?�나??

A. Complete shadcn/ui guide for building beautiful UIs. Learn Radix UI foundation, Tailwind styling, copy-paste components,???�무?�서????본문???�제?� ?�택 가?�드�?참고???�용?�면 ?�니??

Q. ?�행?�로 ?�으�?좋�? 글?�?

A. �?글 ?�단???�전 글 ?�는 관??글 링크�??�라가�??�서?��?배울 ???�습?�다. C++ ?�리�?목차?�서 ?�체 ?�름???�인?????�습?�다.

Q. ??깊이 공�??�려�?

A. cppreference?� ?�당 ?�이브러�?공식 문서�?참고?�세?? 글 말�???참고 ?�료 링크???�용?�면 좋습?�다.


같이 보면 좋�? 글 (?��? 링크)

??주제?� ?�결?�는 ?�른 글?�니??


??글?�서 ?�루???�워??(관??검?�어)

shadcn/ui, Radix UI, Tailwind, UI Components, React, Design System, Frontend ?�으�?검?�하?�면 ??글???��????�니??