shadcn/ui完全ガイド - コピペで実装する美しいUIコンポーネント
shadcn/uiの特徴、導入方法、カスタマイズ手法を徹底解説。Radix UIとTailwind CSSを活用したモダンなコンポーネントライブラリの実践的な使い方
約5分で読めます
技術記事
実践的
この記事のポイント
shadcn/uiの特徴、導入方法、カスタマイズ手法を徹底解説。Radix UIとTailwind CSSを活用したモダンなコンポーネントライブラリの実践的な使い方
この記事では、実践的なアプローチで技術的な課題を解決する方法を詳しく解説します。具体的なコード例とともに、ベストプラクティスを学ぶことができます。
shadcn/uiは、コピー&ペーストで使えるReactコンポーネントの集合体として、2023年から急速に人気を集めているUIライブラリです。従来のnpmパッケージとは異なるアプローチで、開発者に完全な制御権を提供します。
shadcn/uiとは
graph TD A[shadcn/ui] --> B[Radix UI Primitives] A --> C[Tailwind CSS] A --> D[コピペ可能なコード] B --> E[アクセシビリティ] C --> F[スタイリング] D --> G[完全なカスタマイズ性]
主な特徴
- 依存関係なし - npmパッケージではなく、コードをプロジェクトに直接コピー
- 完全なカスタマイズ性 - すべてのコードが手元にあるため、自由に編集可能
- Radix UI基盤 - アクセシビリティが考慮された堅牢な実装
- Tailwind CSS - ユーティリティファーストのスタイリング
- TypeScript対応 - 型安全な開発が可能
セットアップ
1. 前提条件
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tailwindcss": "^3.0.0"
}
}
2. 初期設定
# shadcn/ui CLIのインストール
npx shadcn-ui@latest init
# 設定ファイルが生成される
✔ Would you like to use TypeScript? … yes
✔ 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/utils
3. components.json
生成される設定ファイル:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
コンポーネントの導入
CLIを使用したインストール
# 単一コンポーネント
npx shadcn-ui@latest add button
# 複数コンポーネント
npx shadcn-ui@latest add dialog card table
# すべてのコンポーネント
npx shadcn-ui@latest add --all
生成されるファイル構造
components/
└── ui/
├── button.tsx
├── dialog.tsx
├── card.tsx
└── table.tsx
主要コンポーネントの使い方
Button
import { Button } from "@/components/ui/button"
export function ButtonDemo() {
return (
<div className="flex gap-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>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
<Button asChild>
<a href="/home">Link Button</a>
</Button>
</div>
)
}
Form(React Hook Form + Zod)
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, {
message: "ユーザー名は2文字以上で入力してください",
}),
email: z.string().email({
message: "有効なメールアドレスを入力してください",
}),
})
export 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>ユーザー名</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
公開されるユーザー名です
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>メールアドレス</FormLabel>
<FormControl>
<Input placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">送信</Button>
</form>
</Form>
)
}
データテーブル(Tanstack Table統合)
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface User {
id: string
name: string
email: string
role: string
}
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: "名前",
},
{
accessorKey: "email",
header: "メールアドレス",
},
{
accessorKey: "role",
header: "役割",
},
]
export function DataTableDemo({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<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>
)
}
カスタマイズ手法
1. テーマのカスタマイズ
/* 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.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: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... ダークモードの色定義 ... */
}
}
2. コンポーネントの拡張
// components/ui/button-extended.tsx
import { Button, ButtonProps } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
interface ExtendedButtonProps extends ButtonProps {
loading?: boolean
}
export function ButtonExtended({
loading,
children,
disabled,
...props
}: ExtendedButtonProps) {
return (
<Button disabled={disabled || loading} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
)
}
3. 複合コンポーネントの作成
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
interface StatCardProps {
title: string
value: string | number
description?: string
trend?: "up" | "down" | "neutral"
change?: string
}
export function StatCard({ title, value, description, trend, change }: StatCardProps) {
const trendColors = {
up: "text-green-600",
down: "text-red-600",
neutral: "text-gray-600"
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{trend && change && (
<Badge variant="secondary" className={trendColors[trend]}>
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change}
</Badge>
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<CardDescription className="text-xs">{description}</CardDescription>
)}
</CardContent>
</Card>
)
}
アニメーション統合
graph LR A[shadcn/ui] --> B[Framer Motion] A --> C[CSS Transitions] A --> D[Radix UI Animation] B --> E[複雑なアニメーション] C --> F[シンプルな遷移] D --> G[組み込みアニメーション]
Framer Motionとの統合
import { motion } from "framer-motion"
import { Card } from "@/components/ui/card"
export function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Card>{children}</Card>
</motion.div>
)
}
ベストプラクティス
1. ディレクトリ構造
components/
├── ui/ # shadcn/uiコンポーネント
│ ├── button.tsx
│ ├── card.tsx
│ └── ...
├── composite/ # 複合コンポーネント
│ ├── stat-card.tsx
│ └── data-table.tsx
└── layout/ # レイアウトコンポーネント
├── header.tsx
└── sidebar.tsx
2. 型定義の管理
// types/ui.ts
import { VariantProps } from "class-variance-authority"
import { buttonVariants } from "@/components/ui/button"
export type ButtonVariants = VariantProps<typeof buttonVariants>
3. ユーティリティ関数
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 使用例
<div className={cn(
"rounded-lg border p-3",
isActive && "border-primary",
className
)} />
パフォーマンス最適化
1. 動的インポート
import dynamic from 'next/dynamic'
const DataTable = dynamic(
() => import('@/components/composite/data-table').then(mod => mod.DataTable),
{
loading: () => <TableSkeleton />,
ssr: false
}
)
2. メモ化
import { memo } from 'react'
export const ExpensiveComponent = memo(({ data }: { data: any[] }) => {
// 重い処理
return <div>{/* ... */}</div>
}, (prevProps, nextProps) => {
return prevProps.data.length === nextProps.data.length
})
トラブルシューティング
よくある問題と解決策
-
スタイルが適用されない
- Tailwind設定でコンポーネントディレクトリが含まれているか確認
- CSS変数が正しく定義されているか確認
-
TypeScriptエラー
@types/react
と@types/react-dom
のバージョン確認- tsconfig.jsonのpathsが正しく設定されているか確認
-
ダークモード切り替え
- next-themesまたは独自の実装でクラス切り替えを実装
まとめ
shadcn/uiは、以下の特徴により現代的なReact開発に最適なソリューションです:
- 所有権: コードが手元にあるため完全な制御が可能
- カスタマイズ性: 自由に編集・拡張できる
- 品質: Radix UIベースの堅牢な実装
- 開発体験: CLIによる簡単な導入と管理
プロジェクトの要件に合わせて自由にカスタマイズし、独自のデザインシステムを構築できる点が最大の魅力です。