はじめに
正直なところ、最初は「また新しいUIライブラリかよ」と思っていたんですよ。Material UIとかChakra UIとか、すでに色々試してきた身としては。
でも、shadcn/uiは従来のUIライブラリとは根本的に違う。GitHubで101,000スター以上、26,700以上のプロジェクトで使われているのには理由があります。
30代になって思うのは、技術選定で「人気だから」は危険だけど、「これだけの人が支持しているのには理由がある」という視点も大切だということ。shadcn/uiは後者。使ってみて「あ、これは違う」と感じました。
shadcn/uiとは
shadcn/uiは、Radix UIとTailwind CSSで構築された美しいUIコンポーネント集です。ただし、普通のUIライブラリとは違ってnpmパッケージとしてインストールしない。
コンポーネントのソースコードを自分のプロジェクトにコピーして使う、という新しいアプローチを取っています。
公式サイトでは「A set of beautifully-designed, accessible components and a code distribution platform」と説明されています。つまり、コンポーネントを配布するプラットフォームなんですよ。
最新バージョンはv3.5.1で、MITライセンスのオープンソース。406人以上のコントリビューターが開発に参加しています。
特徴・メリット
1. コンポーネントが自分のものになる
これが最大の特徴。普通のUIライブラリだと、バージョンアップで破壊的変更が入ったり、カスタマイズに限界があったりする。
shadcn/uiは、コンポーネントのソースコードを自分のプロジェクトにコピーするので、完全にカスタマイズ可能。依存関係に縛られない自由さがあります。
個人的には、この「所有権」の感覚が一番のメリットだと思います。
2. Radix UIベースで堅牢
内部的にはRadix UIを使っているので、アクセシビリティがしっかり確保されている。キーボードナビゲーション、スクリーンリーダー対応など、自分で一から実装すると大変な部分が最初から入っています。
これ、意外と重要なんですけど、アクセシビリティ対応は後から入れると本当に大変。最初から入っているのは時短になる。
3. Tailwind CSSとの完璧な統合
Tailwind CSSユーザーには一択ですね。クラス名でスタイリングできるので、既存のTailwindプロジェクトにスムーズに導入できます。
カスタムテーマもtailwind.configで管理できるので、デザインシステムとの整合性が取りやすい。
4. 50以上のコンポーネント
Button、Card、Dialog、Form、Input、Select、Tooltip、Sidebar、Data Table、Chartなど、実用的なコンポーネントが揃っています。
業務アプリで必要になるUIパーツは、ほぼカバーできる印象。コスパ的に、これだけ揃っているのは嬉しい。
5. TypeScriptで型安全
全コンポーネントがTypeScriptで書かれているので、型推論がしっかり効く。propsの補完が効くのは、開発効率に直結します。
インストール方法
前提条件
- Node.js 18以降
- Reactプロジェクト(Next.js、Vite、Remix等)
- Tailwind CSS v3.4以上(v4にも対応)
初期セットアップ
# Next.jsプロジェクトの場合
npx shadcn@latest init
このコマンドで対話式のセットアップが始まります。スタイル、ベースカラー、CSSファイルの場所などを聞かれるので、プロジェクトに合わせて選択。
コンポーネントの追加
# Buttonコンポーネントを追加
npx shadcn@latest add button
# 複数まとめて追加
npx shadcn@latest add button card dialog input
追加されたコンポーネントはcomponents/ui/ディレクトリに配置されます。
パッケージマネージャー
pnpm、npm、yarn、bunすべてに対応しています。
# pnpmの場合
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button
# yarnの場合
npx shadcn@latest init
npx shadcn@latest add button
# bunの場合
bunx --bun shadcn@latest init
bunx --bun shadcn@latest add button
基本的な使い方
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="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
)
}
variantでスタイルを切り替えられる。直感的ですよね。
サイズの指定
<Button size="sm">Small</Button>
<Button>Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<IconPlus />
</Button>
アイコン付きボタン
import { Button } from "@/components/ui/button"
import { Mail } from "lucide-react"
export function ButtonWithIcon() {
return (
<Button>
<Mail className="mr-2 h-4 w-4" />
Login with Email
</Button>
)
}
ローディング状態
import { Button } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
export function ButtonLoading() {
return (
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
)
}
Cardコンポーネント
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export function CardDemo() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>プロジェクト作成</CardTitle>
<CardDescription>新しいプロジェクトを作成します</CardDescription>
</CardHeader>
<CardContent>
<p>プロジェクトの詳細を入力してください。</p>
</CardContent>
<CardFooter>
<Button>作成</Button>
</CardFooter>
</Card>
)
}
Dialogコンポーネント
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
export function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">ダイアログを開く</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>確認</DialogTitle>
<DialogDescription>
この操作を実行してもよろしいですか?
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2">
<Button variant="outline">キャンセル</Button>
<Button>確認</Button>
</div>
</DialogContent>
</Dialog>
)
}
実践的なユースケース
フォームの実装
React Hook Formとの組み合わせが公式で推奨されています。
"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,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const formSchema = z.object({
email: z.string().email("有効なメールアドレスを入力してください"),
password: z.string().min(8, "パスワードは8文字以上で入力してください"),
})
export function LoginForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
})
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="email"
render={({ field }) => (
<FormItem>
<FormLabel>メールアドレス</FormLabel>
<FormControl>
<Input placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>パスワード</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
ログイン
</Button>
</form>
</Form>
)
}
ZodとReact Hook Formの組み合わせで、バリデーションも型安全に書けます。
ダークモード対応
Tailwind CSSのダークモードと連携できます。
// globals.css
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* ... その他の変数 */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... その他の変数 */
}
}
テーマ切り替えはnext-themesを使うのが定番。
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import { Moon, Sun } from "lucide-react"
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" />
</Button>
)
}
コンポーネントのカスタマイズ
shadcn/uiの真価は、コンポーネントを自由にカスタマイズできること。
// components/ui/button.tsx を直接編集
const buttonVariants = cva(
"inline-flex items-center justify-center ...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground ...",
// カスタムバリアントを追加
brand: "bg-gradient-to-r from-blue-500 to-purple-600 text-white hover:opacity-90",
},
size: {
// カスタムサイズを追加
xl: "h-14 px-8 text-lg",
},
},
}
)
ソースが手元にあるので、何でもできる。この自由度がQOL上がりますね。
Material UIやChakra UIとの違い
正直なところ、従来のUIライブラリとは思想が違います。
| 項目 | shadcn/ui | Material UI / Chakra UI |
|---|---|---|
| インストール | コードをコピー | npmパッケージ |
| カスタマイズ | 完全に自由 | テーマ/propsで制限あり |
| バンドルサイズ | 使うものだけ | ライブラリ全体 |
| アップデート | 手動で取捨選択 | 自動(破壊的変更あり) |
| 学習コスト | Tailwind必須 | 各ライブラリの記法 |
個人的には、Tailwind CSSを使っているプロジェクトならshadcn/ui一択ですね。使っていないなら、導入コストも考慮した方がいい。
まとめ
shadcn/uiを導入して感じた変化:
- 開発速度: 美しいUIを素早く作れるようになった
- 自由度: カスタマイズの制限がなくなった
- 保守性: 依存関係のバージョン管理から解放された
- 学習コスト: Tailwind知っていればほぼゼロ
- チーム開発: コンポーネントの共有が簡単になった
30代になって思うのは、ツール選びで「流行っているから」より「自分のプロジェクトに合っているか」が大事だということ。その点で、shadcn/uiはTailwind CSSプロジェクトには最適解だと思います。
特に「UIライブラリの制約に縛られたくない」「でも美しいUIは欲しい」という人には刺さるはず。まだ試していない人は、公式サイトのコンポーネント一覧を見てみてください。「これ全部使えるのか」と驚くと思いますよ。