shadcn/ui実践ガイド 2025 - コピー&ペーストコンポーネントの新しいアプローチ
shadcn/uiはコピー&ペースト方式でコンポーネントを提供する新しいアプローチのUIシステムです。npmパッケージではなくソースコードを直接コピーすることで、カスタマイズ性と保守性を高めます。実際のプロジェクトでの導入手順と実装方法を詳しく解説します。
shadcn/uiはコピー&ペーストで使えるReactコンポーネントライブラリの革命。Radix UIとTailwind CSSを基盤に、美しくアクセシブルなコンポーネントを提供。特徴的なコード配布アプローチ、CLIセットアップ、カスタマイズ、実践的なプロジェクト構築まで完全ガイド。
shadcn/ui は、2024-2025 年で最も注目を集めている Reactコンポーネントライブラリです。従来の npmパッケージの概念を覆し、コードを直接プロジェクトに取り込む革新的なアプローチで、美しくアクセシブルなコンポーネントを提供します。
shadcn/ui は、従来の UI ライブラリの概念を根本から覆す革新的なアプローチです:
項目 | shadcn/ui | Material-UI | Chakra UI | Ant Design |
---|---|---|---|---|
配布方式 | コードコピー | npmパッケージ | npmパッケージ | npmパッケージ |
カスタマイズ性 | 非常に高い | 中程度 | 高い | 中程度 |
バンドルサイズ | 必要最小 | 大きい | 中程度 | 非常に大きい |
アクセシビリティ | 優秀 | 良い | 優秀 | 普通 |
スタイル系 | Tailwind CSS | CSS-in-JS | CSS-in-JS | Less/CSS |
shadcn/ui を使用するためには以下が必要です:
# Next.jsプロジェクトの作成
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app
# shadcn/ui CLIのインストール
npx shadcn@latest init
# Vite + Reactプロジェクトの作成
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
# Tailwind CSSのセットアップ
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# shadcn/uiの初期化
npx shadcn@latest init
# Remixプロジェクトの作成
npx create-remix@latest my-app
cd my-app
# Tailwind CSSとshadcn/uiのセットアップ
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npx shadcn@latest init
# Astro + Reactプロジェクトの作成
npm create astro@latest my-app -- --template minimal
cd my-app
npx astro add react tailwind
# shadcn/uiのセットアップ
npx shadcn@latest init
CLI 初期化時に以下の設定を行います:
npx shadcn@latest init
設定項目:
src/components
または components
src/lib/utils
または lib/utils
# Buttonコンポーネントのインストール
npx shadcn@latest add button
コンポーネントの基本使用方法:
import { Button } from '@/components/ui/button';
function MyComponent() {
return (
<div className="space-x-4">
<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>
);
}
# Card関連コンポーネントのインストール
npx shadcn@latest add card
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
function ProductCard() {
return (
<Card className="w-96">
<CardHeader>
<CardTitle>shadcn/uiマスターコース</CardTitle>
<CardDescription>
モダンなReact開発のための完全ガイド
</CardDescription>
</CardHeader>
<CardContent>
<p>コンポーネントライブラリの革命を体験し、美しいUIを効率的に構築する方法を学びましょう。</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">詳細を見る</Button>
<Button>購入する</Button>
</CardFooter>
</Card>
);
}
# フォーム関連コンポーネントのインストール
npx shadcn@latest add form input label button
React Hook Form と Zod を組み合わせた型安全なフォーム:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
const contactSchema = z.object({
name: z.string().min(2, '名前は2文字以上で入力してください'),
email: z.string().email('有効なメールアドレスを入力してください'),
message: z.string().min(10, 'メッセージは10文字以上で入力してください')
});
type ContactFormData = z.infer<typeof contactSchema>;
function ContactForm() {
const form = useForm<ContactFormData>({
resolver: zodResolver(contactSchema),
defaultValues: {
name: '',
email: '',
message: ''
}
});
const onSubmit = (data: ContactFormData) => {
console.log('フォームデータ:', data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>お名前</FormLabel>
<FormControl>
<Input placeholder="山田太郎" {...field} />
</FormControl>
<FormDescription>
公開される表示名です。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>メールアドレス</FormLabel>
<FormControl>
<Input type="email" placeholder="example@company.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>メッセージ</FormLabel>
<FormControl>
<textarea
className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="お問い合わせ内容をこちらに記載してください"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
送信する
</Button>
</form>
</Form>
);
}
shadcn/ui は、CSS 変数を使用した柔軟なテーマシステムを提供します:
/* globals.css */
@layer base {
:root {
/* ライトテーマ */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--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: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
/* ダークテーマ */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--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: 224.3 76.3% 94.1%;
}
}
:root {
--primary: 221.2 83.2% 53.3%; /* ブルー */
--secondary: 210 40% 96%; /* グレー */
--accent: 210 40% 96%; /* グレー */
}
:root {
--primary: 262 100% 47%; /* パープル */
--secondary: 230 100% 60%; /* ブルー */
--accent: 340 100% 60%; /* ピンク */
}
:root {
--primary: 221.2 83.2% 53.3%; /* ブルー */
--secondary: 210 40% 96%; /* グレー */
--accent: 210 40% 96%; /* グレー */
}
:root {
--primary: 262 100% 47%; /* パープル */
--secondary: 230 100% 60%; /* ブルー */
--accent: 340 100% 60%; /* ピンク */
}
// app/layout.tsx
import { ThemeProvider } from '@/components/theme-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
// components/theme-toggle.tsx
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
}
# データテーブル関連コンポーネント
npx shadcn@latest add table checkbox dropdown-menu
TanStack Table との統合例:
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { ArrowUpDown } from 'lucide-react';
type User = {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
};
const columns: ColumnDef<User>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
},
{
accessorKey: 'name',
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
名前
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
},
{
accessorKey: 'email',
header: 'メール',
},
{
accessorKey: 'role',
header: '役職',
},
{
accessorKey: 'status',
header: 'ステータス',
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'
: 'bg-red-100 text-red-800'
}`}
>
{status === 'active' ? 'アクティブ' : '非アクティブ'}
</span>
);
},
},
];
function UserTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{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">
データがありません
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
shadcn/ui のコードコピーアプローチによるメリット:
// 適切な Tree Shaking 設定
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['@radix-ui/react-icons'],
},
};
// モーダルコンポーネントの遅延読み込み
import { lazy, Suspense } from 'react';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
const HeavyComponent = lazy(() => import('@/components/heavy-component'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<Button onClick={() => setShowHeavy(true)}>
重いコンポーネントを読み込み
</Button>
{showHeavy && (
<Suspense fallback={<Skeleton className="w-full h-64" />}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
// app/dashboard/layout.tsx
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<h1 className="text-lg font-semibold">ダッシュボード</h1>
</header>
<main className="flex-1 p-4">{children}</main>
</SidebarInset>
</SidebarProvider>
);
}
// components/hero-section.tsx
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
function HeroSection() {
return (
<section className="container flex flex-col items-center gap-4 pb-8 pt-6 md:py-10">
<div className="flex max-w-[980px] flex-col items-start gap-2">
<Badge variant="outline">新登場</Badge>
<h1 className="text-3xl font-bold leading-tight tracking-tighter md:text-5xl lg:leading-[1.1]">
コンポーネントライブラリの
<br className="hidden sm:inline" />
新しいスタンダード
</h1>
<p className="max-w-[750px] text-lg text-muted-foreground sm:text-xl">
美しくアクセシブルなコンポーネントで、
あなたのアプリケーションを構築しましょう。
</p>
</div>
<div className="flex gap-4">
<Button size="lg">始める</Button>
<Button variant="outline" size="lg">
GitHubで確認
</Button>
</div>
</section>
);
}
shadcn/ui は Radix UI を基盤としており、標準で高いアクセシビリティを提供します:
// キーボード操作をサポートするメニュー
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from '@/components/ui/navigation-menu';
function AccessibleNavigation() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>サービス</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-4 md:w-[400px] lg:w-[500px]">
<li className="row-span-3">
<NavigationMenuLink asChild>
<a
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href="/"
>
<div className="mb-2 mt-4 text-lg font-medium">
shadcn/ui
</div>
<p className="text-sm leading-tight text-muted-foreground">
美しくアクセシブルなコンポーネント
</p>
</a>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);
}
// スクリーンリーダーへの適切な情報提供
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
function AccessibleButton() {
return (
<Button
aria-label="新しい項目を追加"
aria-describedby="add-item-description"
>
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
追加
</Button>
);
}
project/
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn/uiコンポーネント
│ │ │ ├── button.tsx
│ │ │ ├── input.tsx
│ │ │ └── form.tsx
│ │ ├── custom/ # カスタムコンポーネント
│ │ │ ├── user-profile.tsx
│ │ │ └── dashboard-card.tsx
│ │ └── layout/ # レイアウトコンポーネント
│ │ ├── header.tsx
│ │ └── sidebar.tsx
│ ├── lib/
│ │ ├── utils.ts # ユーティリティ関数
│ │ └── constants.ts # 定数定義
│ └── styles/
│ └── globals.css # グローバルCSS
└── components.json # shadcn/ui設定
shadcn/ui は、Reactコンポーネントライブラリの概念を根本から変える革命的なアプローチです:
従来の UI ライブラリの制約を脱却し、真に柔軟で持続可能な UI 開発を実現する shadcn/ui を、次のプロジェクトでぜひお試しください。