shadcn/ui の思想を徹底解剖:なぜ「コピーして使う」アプローチが拡張性に強いのか

React の UI 開発において、shadcn/ui は従来のコンポーネントライブラリとは一線を画す革新的なアプローチで注目を集めています。npm パッケージとしてインストールするのではなく、「コピーして使う」という斬新な思想で開発者コミュニティに大きな衝撃を与えました。
この記事では、shadcn/ui がなぜこのような独特なアプローチを採用したのか、そしてそれがどのように拡張性と保守性の向上につながるのかを詳しく解説します。従来の UI ライブラリの制約から解放される新しい開発体験を、具体的な実装例とともにご紹介していきましょう。
背景
従来の UI ライブラリの課題
React エコシステムにおいて、UI ライブラリは長らく開発効率向上の重要な要素でした。Material-UI(現 MUI)、Ant Design、Chakra UI など、多くの優秀なライブラリが開発現場を支えてきています。
しかし、これらの従来型ライブラリには共通する構造的な問題が存在していました。以下の図で、従来のライブラリアーキテクチャの構造を示します。
mermaidflowchart TB
app[アプリケーション] --> lib[UI ライブラリ]
lib --> deps[大量の依存関係]
deps --> core[コアライブラリ]
deps --> styles[スタイルシステム]
deps --> icons[アイコンセット]
lib --> bundle[バンドルサイズ増加]
lib --> lock[ベンダーロックイン]
lib --> custom[カスタマイズ制約]
従来のライブラリでは、開発者は完全なパッケージを依存関係として追加する必要がありました。これにより、使用しない機能も含めた全体がプロジェクトに組み込まれ、バンドルサイズの増大やアップデート時の破壊的変更のリスクが生じていたのです。
コンポーネントライブラリの進化
UI ライブラリの進化を振り返ると、大きく 3 つの段階に分けることができます。
# | 段階 | 特徴 | 代表例 |
---|---|---|---|
1 | 第一世代 | モノリシックなライブラリ | Bootstrap、Foundation |
2 | 第二世代 | コンポーネント指向 | Material-UI、Ant Design |
3 | 第三世代 | ヘッドレス+コピーアプローチ | shadcn/ui、Radix UI |
第三世代の特徴は、UI ロジックとスタイルを分離し、開発者がより柔軟にカスタマイズできる環境を提供することです。shadcn/ui はこの第三世代の代表格として、まったく新しいパラダイムを提示しました。
課題
既存ライブラリの制約
従来の UI ライブラリを使用する際、開発者は以下のような制約に直面することが多々ありました。
デザインシステムの固定化
Material-UI を例に取ると、Google のマテリアルデザインの思想が強く反映されています。独自のデザインシステムを構築したい場合、大幅なオーバーライドが必要となり、結果的にライブラリの恩恵を受けにくくなってしまいます。
typescript// Material-UI でのカスタマイズ例(複雑な設定が必要)
import {
createTheme,
ThemeProvider,
} from '@mui/material/styles';
const customTheme = createTheme({
palette: {
primary: {
main: '#custom-color',
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
// 大量のオーバーライド設定...
},
},
},
},
});
バンドルサイズの課題
多機能なライブラリほど、使用しない機能も含めてバンドルに含まれがちです。Tree Shaking が効かない部分も多く、最終的なアプリケーションサイズが肥大化する傾向にありました。
依存関係の問題
現代のフロントエンド開発において、依存関係の管理は複雑さを増し続けています。以下の図で、典型的な依存関係の構造を示します。
mermaidflowchart LR
project[プロジェクト] --> uiLib[UI ライブラリ]
uiLib --> emotion[Emotion/Styled]
uiLib --> react[React]
uiLib --> utils[Utility 群]
emotion --> deps1[依存関係群]
utils --> deps2[依存関係群]
deps1 --> security[セキュリティリスク]
deps2 --> version[バージョン競合]
uiLib --> update[アップデート時]
update --> breaking[破壊的変更]
breaking --> migration[マイグレーション作業]
特に問題となるのは、ライブラリのメジャーアップデート時です。内部で使用している依存関係の変更により、予期しない破壊的変更が発生し、大規模な修正作業が必要になることも珍しくありません。
カスタマイズの限界
従来のライブラリでカスタマイズを行う際、以下のような課題が発生していました。
CSS-in-JS の制約
typescript// 既存ライブラリでの制約例
const StyledButton = styled(MuiButton)`
// ライブラリの内部スタイルを上書きする必要
&& {
background-color: ${(props) => props.theme.primary};
// 詳細度の問題でスタイルが適用されない場合も...
}
`;
コンポーネントの内部構造への依存
ライブラリが提供するコンポーネントの内部構造に依存したカスタマイズは、ライブラリのアップデート時に動作しなくなるリスクを抱えています。
解決策
「コピーして使う」思想の核心
shadcn/ui が提唱する「コピーして使う」アプローチは、従来の依存関係モデルを根本的に覆す革新的な考え方です。
以下の図で、shadcn/ui のアーキテクチャと従来のライブラリとの違いを示します。
mermaidflowchart TB
subgraph traditional [従来のアプローチ]
app1[アプリケーション] --> package[npm パッケージ]
package --> blackbox[ブラックボックス]
end
subgraph shadcn [shadcn/ui アプローチ]
app2[アプリケーション] --> cli[shadcn CLI]
cli --> copy[コンポーネントコピー]
copy --> source[ソースコード]
source --> customize[自由なカスタマイズ]
end
traditional --> deps[依存関係リスク]
shadcn --> ownership[完全な所有権]
このアプローチの核心は、開発者がコンポーネントの完全な所有権を持つ ことです。npm パッケージとして提供されるのではなく、必要なコンポーネントのソースコードを直接プロジェクトにコピーします。
CLI ツールによる効率化
shadcn/ui では、専用の CLI ツールが提供されており、必要なコンポーネントを簡単に追加できます。
bash# コンポーネントの追加
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add form
このコマンドを実行すると、指定されたコンポーネントのソースコードがプロジェクトの components/ui
ディレクトリにコピーされます。
拡張性を重視した設計哲学
shadcn/ui の設計哲学は、拡張性とカスタマイズ性の最大化 にあります。
Radix UI との戦略的連携
shadcn/ui は UI ロジック部分で Radix UI を活用しています。Radix UI はヘッドレス UI ライブラリとして、アクセシビリティやキーボードナビゲーションなどの複雑な UI ロジックを提供します。
typescript// Button コンポーネントの構造例
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 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',
// バリエーションの定義...
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
Class Variance Authority(CVA)の活用
コンポーネントのバリエーション管理には CVA を使用し、TypeScript の型安全性を保ちながら柔軟なスタイリングを実現しています。
開発者の自由度向上
shadcn/ui のアプローチにより、開発者は以下の自由度を獲得できます。
完全なカスタマイズ権限
コンポーネントのソースコードを直接所有するため、どのような修正も自由に行えます。
typescript// 自由なカスタマイズ例
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
// 独自のプロパティを追加
customBehavior?: boolean;
analyticsEvent?: string;
}
const Button = React.forwardRef<
HTMLButtonElement,
ButtonProps
>(
(
{
className,
variant,
size,
asChild = false,
customBehavior,
analyticsEvent,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
const handleClick = (
event: React.MouseEvent<HTMLButtonElement>
) => {
// カスタムロジックの追加
if (customBehavior) {
// 独自の処理...
}
if (analyticsEvent) {
// アナリティクス送信...
}
props.onClick?.(event);
};
return (
<Comp
className={cn(
buttonVariants({ variant, size, className })
)}
ref={ref}
onClick={handleClick}
{...props}
/>
);
}
);
具体例
実際のコンポーネント実装例
shadcn/ui のコンポーネントがどのように実装されているか、実際の Dialog コンポーネントを例に見てみましょう。
基本的な Dialog 構造
typescript// Dialog のインポートと基本設定
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
カスタムスタイルの適用
typescript// Dialog のオーバーレイ部分
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<
typeof DialogPrimitive.Overlay
>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
));
コンテンツエリアの実装
typescript// Dialog のメインコンテンツ
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<
typeof DialogPrimitive.Content
>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]',
'gap-4 border bg-background p-6 shadow-lg duration-200',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',
'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
'sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className='absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground'>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
カスタマイズパターン
shadcn/ui のコンポーネントは、様々なレベルでカスタマイズが可能です。
Level 1:CSS クラスの調整
typescript// 基本的なスタイル調整
<Button className='bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600'>
グラデーションボタン
</Button>
Level 2:バリアント追加
typescript// buttonVariants に新しいバリアントを追加
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',
// 独自バリアントの追加
gradient:
'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600',
neon: 'bg-black text-cyan-400 border border-cyan-400 hover:bg-cyan-400 hover:text-black transition-all duration-300 shadow-lg shadow-cyan-400/50',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
// カスタムサイズ
xl: 'h-14 rounded-lg px-12 text-lg',
},
},
}
);
Level 3:完全なコンポーネント再実装
typescript// プロジェクト固有の要件に合わせた完全カスタマイズ
const CustomButton = React.forwardRef<
HTMLButtonElement,
ButtonProps
>(
(
{
className,
variant,
size,
asChild = false,
loading,
icon,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(
buttonVariants({ variant, size, className })
)}
ref={ref}
disabled={loading || props.disabled}
{...props}
>
{loading && (
<Spinner className='mr-2 h-4 w-4 animate-spin' />
)}
{icon && !loading && (
<span className='mr-2'>{icon}</span>
)}
{props.children}
</Comp>
);
}
);
他ライブラリとの比較
以下の表で、shadcn/ui と他の主要ライブラリの特徴を比較します。
# | 項目 | shadcn/ui | Material-UI | Ant Design | Chakra UI |
---|---|---|---|---|---|
1 | インストール方法 | コピー&ペースト | npm パッケージ | npm パッケージ | npm パッケージ |
2 | バンドルサイズ | 必要分のみ | 大きい | 大きい | 中程度 |
3 | カスタマイズ性 | 完全自由 | 制限あり | 制限あり | 高い |
4 | 依存関係リスク | なし | あり | あり | あり |
5 | アップデート影響 | なし | 破壊的変更あり | 破壊的変更あり | 中程度 |
6 | 学習コスト | 低い | 高い | 中程度 | 中程度 |
以下の図で、各ライブラリのアプローチの違いを視覚的に示します。
mermaidflowchart TD
subgraph comparison [ライブラリ比較]
subgraph mui [Material-UI]
muiApp[アプリ] --> muiPackage[MUIパッケージ]
muiPackage --> muiTheme[テーマシステム]
muiTheme --> muiCustom[限定的カスタマイズ]
end
subgraph shadcn [shadcn/ui]
shadcnApp[アプリ] --> shadcnCLI[CLI]
shadcnCLI --> shadcnCopy[ソースコピー]
shadcnCopy --> shadcnFree[完全自由]
end
subgraph chakra [Chakra UI]
chakraApp[アプリ] --> chakraPackage[Chakraパッケージ]
chakraPackage --> chakraSystem[デザインシステム]
chakraSystem --> chakraCustom[高いカスタマイズ性]
end
end
muiCustom --> constraint[制約あり]
shadcnFree --> freedom[完全自由]
chakraCustom --> balance[バランス型]
この比較から分かるように、shadcn/ui は完全な自由度を提供する一方で、他のライブラリはそれぞれ異なる制約とメリットを持っています。
まとめ
shadcn/ui の「コピーして使う」アプローチは、UI ライブラリの新しいパラダイムを提示しています。従来の npm パッケージモデルから脱却し、開発者にコンポーネントの完全な所有権を与えることで、以下のメリットを実現しました。
主要なメリット
- 依存関係リスクの完全な排除
- 無制限のカスタマイズ自由度
- バンドルサイズの最適化
- アップデート時の破壊的変更からの解放
拡張性の観点から
- プロジェクト固有の要件に完全対応
- 段階的な機能追加が容易
- チーム独自のデザインシステム構築が可能
この革新的なアプローチは、特に長期的なプロジェクトや、独自性の高いデザインシステムを必要とするプロダクトにおいて、その真価を発揮します。開発者が真の意味でコンポーネントを「所有」することで、制約のない自由な開発体験を提供してくれるのです。
shadcn/ui は単なる UI ライブラリを超えて、フロントエンド開発の新しい可能性を切り開いています。この思想を理解し活用することで、より柔軟で保守性の高いアプリケーション開発が実現できるでしょう。
関連リンク
- article
shadcn/ui の思想を徹底解剖:なぜ「コピーして使う」アプローチが拡張性に強いのか
- article
shadcn/ui でダッシュボードをデザインするベストプラクティス
- article
shadcn/ui のコンポーネント一覧と使い方まとめ
- article
shadcn/ui × Next.js:モダンな UI を爆速構築する方法
- article
shadcn/ui と Chakra UI/Material UI の違いを徹底比較
- article
shadcn/ui のインストールと初期設定ガイド【初心者向け】
- article
【比較検証】Convex vs Firebase vs Supabase:リアルタイム性・整合性・学習コストの最適解
- article
【徹底比較】Preact vs React 2025:バンドル・FPS・メモリ・DX を総合評価
- article
GPT-5-Codex vs Claude Code / Cursor 徹底比較:得意領域・精度・開発速度の違いを検証
- article
Astro × Cloudflare Workers/Pages:エッジ配信で超高速なサイトを構築
- article
【2025 年版】Playwright vs Cypress vs Selenium 徹底比較:速度・安定性・学習コストの最適解
- article
Apollo を最短導入:Vite/Next.js/Remix での初期配線テンプレ集
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来