サイトアイコンmaita tomoya dev io

#16 shadcn/uiが変えるフロントエンド開発!従来のUIライブラリとは一線を画す新アプローチの全貌

はじめに:なぜshadcn/uiが注目されているのか?

2023年、JavaScriptエコシステムで異例の現象が起きました。「shadcn/ui」が、JavaScript Rising Starsで総合1位を獲得。2位のBunに10,000スター以上の差をつけての圧勝でした。さらに2024年も連続で1位を獲得し、その勢いは止まりません。

なぜこれほどまでに注目されているのでしょうか?

その答えは、shadcn/uiの公式サイトに明確に示されています:

"This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps." (これはコンポーネントライブラリではありません。アプリにコピー&ペーストできる再利用可能なコンポーネントのコレクションです。)

今回は、このshadcn/uiが従来のUIライブラリとどう違うのか、なぜ開発者から支持されているのか、そして実際にどう使うのかを、初心者エンジニアにもわかりやすく解説していきます。

shadcn/uiとは何か?基本概念の理解

読み方と名前の由来

まず、「shadcn/ui」は「シャドシーエヌ・ユーアイ」と読みます。開発者のShad氏(@shadcn)の名前から取られています。

従来のUIライブラリとの決定的な違い

従来のUIライブラリ(Material-UI、Ant Design、Chakra UIなど)の仕組み

# 従来のUIライブラリのインストール
npm install @mui/material @emotion/react @emotion/styled
// 使用例:Material-UI
import { Button } from '@mui/material';

function App() {
  return <Button variant="contained">Click me</Button>;
}

特徴:

  • npmパッケージとしてインストール
  • node_modulesに依存
  • カスタマイズは限定的(テーマやpropsで調整)
  • ライブラリ全体のバンドルサイズが大きい

バンドルサイズとは? バンドル(Bundle)とは、複数のJavaScriptファイルやCSSファイル、その他のリソースを1つまたは少数のファイルにまとめたものです。WebpackやRollupなどの「バンドラー」と呼ばれるツールが、開発時に分かれている多数のファイルを本番環境用に結合・圧縮します。

バンドルサイズとは、このまとめられたファイルの容量のことで、ユーザーがWebサイトを開く際にダウンロードする必要があるデータ量を指します。バンドルサイズが大きいと:

  • ページの読み込みが遅くなる
  • モバイル環境では特に影響が大きい
  • データ通信量が増える

Material-UIのimportについて import { Button } from '@mui/material'のように特定のコンポーネントだけをimportしても、実はMaterial-UI全体が読み込まれるわけではありません。現代のバンドラーは「Tree Shaking」という機能を持っており、使用されていないコードを自動的に除外します。

ただし、Material-UIの場合:

  • Buttonコンポーネント自体が依存する共通コード(テーマシステム、スタイルエンジンなど)も含まれる
  • これらの基盤となるコードが約30-40KBある
  • 結果として、1つのコンポーネントを使うだけでも相当なサイズになる

shadcn/uiの仕組み

# shadcn/uiのコンポーネント追加
npx shadcn-ui@latest add button
// 使用例:shadcn/ui
// components/ui/button.tsx(自動生成されたファイル)
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",
        outline: "border border-input bg-background hover:bg-accent",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)

// 使用側
import { Button } from "@/components/ui/button"

function App() {
  return (
    <div className="space-y-4">
      {/* デフォルトのボタン */}
      <Button>Click me</Button>
      
      {/* バリアントを指定 */}
      <Button variant="destructive">削除</Button>
      <Button variant="outline">キャンセル</Button>
      <Button variant="secondary">サブアクション</Button>
      <Button variant="ghost">ゴースト</Button>
      <Button variant="link">リンク</Button>
      
      {/* サイズを指定 */}
      <Button size="sm">小さいボタン</Button>
      <Button size="lg">大きいボタン</Button>
      
      {/* 組み合わせも可能 */}
      <Button variant="outline" size="lg">大きいアウトライン</Button>
    </div>
  )
}

buttonVariantsの仕組み: cva(Class Variance Authority)を使って、ボタンのバリアント(種類)を定義しています。これにより:

  • variantプロパティで見た目のスタイルを切り替え可能
  • sizeプロパティでサイズを調整可能
  • TypeScriptの型安全性が保証される(存在しないvariantを指定するとエラーになる)

特徴:

  • コンポーネントのコードをプロジェクトに直接コピー
  • 完全にカスタマイズ可能(コードを直接編集できる)
  • 必要なコンポーネントだけを追加
  • バンドルサイズを最小限に抑えられる

shadcn/uiの技術スタック

1. Radix UI(ラディックス・ユーアイ)

Radix UIとは?

  • スタイルを持たない(Unstyled/Headless)UIコンポーネントライブラリ
  • アクセシビリティ(障害を持つ人でも使いやすい設計)が組み込まれている
  • WAI-ARIA(Web Accessibility Initiative - Accessible Rich Internet Applications)準拠

アクセシビリティとは? Webアクセシビリティとは、障害の有無や年齢、利用環境などに関わらず、すべての人がWebサイトやアプリケーションを利用できるようにすることです。Radix UIは以下のような機能を自動的に提供します:

// Radix UIの例(スタイルなし)
import * as Dialog from '@radix-ui/react-dialog';

<Dialog.Root>
  <Dialog.Trigger>開く</Dialog.Trigger>  {/* ← キーボード操作対応 */}
  <Dialog.Portal>
    <Dialog.Overlay />  {/* ← 背景のクリックで閉じる */}
    <Dialog.Content>
      {/* ↓ スクリーンリーダー用のタイトル(role="heading"が自動付与) */}
      <Dialog.Title>タイトル</Dialog.Title>
      {/* ↓ 説明文(aria-describedbyで関連付け) */}
      <Dialog.Description>説明文</Dialog.Description>
      {/* ↓ Escキーで閉じる、フォーカストラップ対応 */}
      <Dialog.Close>閉じる</Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Radix UIが自動的に処理してくれること:

  • キーボード操作: Tab、Enter、Escape、矢印キーなどの適切な処理
  • フォーカス管理: ダイアログを開いた時のフォーカス移動、閉じた時の復帰
  • ARIA属性: rolearia-labelaria-describedbyなどの自動付与
  • スクリーンリーダー対応: 視覚障害者向けの読み上げソフトへの対応

これらを自分で実装すると非常に複雑になりますが、Radix UIを使えば自動的に対応されます。

2. Tailwind CSS(テイルウィンド・シーエスエス)

Tailwind CSSとは?

  • ユーティリティファーストのCSSフレームワーク
  • クラス名を組み合わせてスタイリングする
<!-- 従来のCSS -->
<button class="btn btn-primary">Click</button>
<style>
  .btn { padding: 8px 16px; border-radius: 4px; }
  .btn-primary { background-color: blue; color: white; }
</style>

<!-- Tailwind CSS -->
<button class="px-4 py-2 rounded bg-blue-500 text-white">Click</button>

3. Class Variance Authority (CVA)

CVAとは?

  • コンポーネントのバリアント(種類)を管理するライブラリ
  • TypeScript対応で型安全
const buttonVariants = cva(
  // ベーススタイル
  "inline-flex items-center justify-center rounded-md",
  {
    variants: {
      // バリアント定義
      variant: {
        default: "bg-primary text-white",
        secondary: "bg-secondary text-black",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        sm: "h-9 px-3 text-xs",
        md: "h-10 px-4 py-2",
        lg: "h-11 px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  }
)

なぜshadcn/uiを使うのか?解決される問題と恩恵

問題1:カスタマイズの制限

従来のUIライブラリの問題点:

// Material-UIでボタンをカスタマイズしたい場合
import { Button } from '@mui/material';
import { styled } from '@mui/material/styles';

// スタイルのオーバーライドが必要
const CustomButton = styled(Button)(({ theme }) => ({
  backgroundColor: '#custom-color',
  '&:hover': {
    backgroundColor: '#hover-color',
  },
  // !importantを使わざるを得ない場合も...
  padding: '12px 24px !important',
}));

shadcn/uiの解決策:

// button.tsxを直接編集
const buttonVariants = cva(
  "...",
  {
    variants: {
      variant: {
        // 新しいバリアントを追加
        custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg",
      },
    },
  }
)

// 使用
<Button variant="custom">カスタムボタン</Button>

なぜこれで問題が解決するのか?

  • 直接編集可能: コンポーネントのソースコードがプロジェクト内にあるため、自由に編集できる
  • !important不要: ライブラリのスタイルを上書きする必要がないため、CSSの優先順位で悩まない
  • TypeScript対応: 新しいバリアントを追加すると、自動的に型定義も更新される
  • バージョン依存なし: ライブラリのアップデートでカスタマイズが壊れる心配がない

問題2:バンドルサイズの肥大化

従来のUIライブラリ:

  • Material-UI:約100KB
  • Ant Design:約300KB+
  • Bootstrap:約81KB

shadcn/uiの場合:

  • 必要なコンポーネントだけを含める
  • Button componentのみ:約2-3KB
  • 使わない機能は含まれない

問題3:デザインの画一化

従来の問題:

// どのサイトも似たようなMaterial Designに...
import { Button, Card, TextField } from '@mui/material';

// カスタマイズが難しく、結果的に似たデザインになりがち

shadcn/uiの解決:

// colors.cssで独自のカラースキームを定義
:root {
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 217.2 32.6% 17.5%;
  // 完全にオリジナルのデザインシステムを構築可能
}

実践!shadcn/uiの導入とカスタマイズ

Step 1: プロジェクトのセットアップ

# Next.jsプロジェクトの作成
npx create-next-app@latest my-app --typescript --tailwind --app

cd my-app

# shadcn/uiの初期化
npx shadcn-ui@latest init

npx shadcn-ui@latest initの仕組み npxは、npmパッケージを一時的にダウンロードして実行するコマンドです。shadcn-uiはnpmに公開されているCLI(コマンドラインインターフェース)ツールで、以下の処理を行います:

  1. プロジェクトの設定ファイルを分析
  2. 必要な設定ファイルを生成(components.json)
  3. ユーティリティ関数を作成(lib/utils.ts)
  4. TailwindCSSの設定を更新
  5. グローバルCSSにベーススタイルを追加

つまり、shadcn-ui自体はプロジェクトに残らず、初期設定だけを行って終了します。

初期化時の質問:

✔ Would you like to use TypeScript (recommended)? … yes
# TypeScriptを使用するか(推奨)

✔ Which style would you like to use? › Default
# スタイルテーマの選択(Default/New York)
# Defaultは角が丸く、New Yorkはより角張ったデザイン

✔ Which color would you like to use as base color? › Slate
# ベースカラーの選択(Slate/Gray/Zinc/Neutral/Stone)

✔ Where is your global CSS file? … app/globals.css
# グローバルCSSファイルの場所(ベーススタイルを追加する場所)

✔ Do you want to use CSS variables for colors? … yes
# CSS変数を使用するか(ダークモード対応などが簡単になる)

✔ Where is your tailwind.config.js located? … tailwind.config.js
# Tailwind設定ファイルの場所

✔ Configure the import alias for components? … @/components
# コンポーネントのインポートエイリアス(import時のパス)

✔ Configure the import alias for utils? … @/lib/utils
# ユーティリティ関数のインポートエイリアス

Step 2: コンポーネントの追加

# よく使うコンポーネントを追加
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add form
npx shadcn-ui@latest add input

コンポーネント追加時の動作 npx shadcn-ui@latest add buttonを実行すると:

  1. shadcn/uiのGitHubリポジトリから最新のButtonコンポーネントのコードを取得
  2. components/ui/button.tsxファイルを作成
  3. 必要な依存関係(Radix UIなど)があれば自動的にnpm installを実行
  4. 完了後、import { Button } from "@/components/ui/button"で使用可能になる

実際に作成されるファイルの例:

components/
  ui/
    button.tsx      # ← ここにコンポーネントのコードがコピーされる
    card.tsx
    dialog.tsx
    form.tsx
    input.tsx

重要な点は、これらのファイルはあなたのプロジェクトの一部になることです。外部ライブラリへの依存ではなく、プロジェクト内のソースコードとして存在します。

Step 3: 実際のコンポーネント作成例

例1:ログインフォームの作成

// app/login/page.tsx
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function LoginPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <Card className="w-[400px]">
        <CardHeader>
          <CardTitle>ログイン</CardTitle>
          <CardDescription>
            アカウントにログインしてください
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="email">メールアドレス</Label>
            <Input 
              id="email" 
              type="email" 
              placeholder="example@email.com"
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="password">パスワード</Label>
            <Input 
              id="password" 
              type="password"
            />
          </div>
        </CardContent>
        <CardFooter className="flex justify-between">
          <Button variant="outline">キャンセル</Button>
          <Button>ログイン</Button>
        </CardFooter>
      </Card>
    </div>
  )
}

例2:ダークモード対応のダッシュボード

// components/Dashboard.tsx
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Moon, Sun } from "lucide-react"

export default function Dashboard() {
  const [isDark, setIsDark] = useState(false)

  const toggleTheme = () => {
    setIsDark(!isDark)
    document.documentElement.classList.toggle('dark')
  }

  return (
    <div className={`min-h-screen ${isDark ? 'dark' : ''}`}>
      <div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
        <header className="p-4 border-b dark:border-gray-700">
          <div className="flex justify-between items-center">
            <h1 className="text-2xl font-bold">ダッシュボード</h1>
            <Button 
              variant="ghost" 
              size="icon"
              onClick={toggleTheme}
            >
              {isDark ? <Sun /> : <Moon />}
            </Button>
          </div>
        </header>
        
        <main className="p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
          <Card>
            <CardHeader>
              <CardTitle>売上</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-3xl font-bold">¥1,234,567</p>
              <p className="text-sm text-gray-500 dark:text-gray-400">
                前月比 +12.5%
              </p>
            </CardContent>
          </Card>
          
          <Card>
            <CardHeader>
              <CardTitle>ユーザー数</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-3xl font-bold">8,934</p>
              <p className="text-sm text-gray-500 dark:text-gray-400">
                前月比 +8.2%
              </p>
            </CardContent>
          </Card>
          
          <Card>
            <CardHeader>
              <CardTitle>コンバージョン率</CardTitle>
            </CardHeader>
            <CardContent>
              <p className="text-3xl font-bold">3.45%</p>
              <p className="text-sm text-gray-500 dark:text-gray-400">
                前月比 +0.3%
              </p>
            </CardContent>
          </Card>
        </main>
      </div>
    </div>
  )
}

Step 4: カスタマイズ例

カスタムボタンバリアントの追加

// components/ui/button.tsx を編集
const buttonVariants = cva(
  "...",
  {
    variants: {
      variant: {
        // 既存のバリアント
        default: "...",
        destructive: "...",
        
        // 新しいグラデーションバリアントを追加
        gradient: "bg-gradient-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700 shadow-lg",
        
        // ネオンエフェクトバリアント
        neon: "bg-transparent border-2 border-cyan-400 text-cyan-400 hover:bg-cyan-400 hover:text-gray-900 transition-all duration-300 shadow-[0_0_10px_rgba(34,211,238,0.5)]",
      },
    },
  }
)

// 使用例
<Button variant="gradient">グラデーションボタン</Button>
<Button variant="neon">ネオンボタン</Button>

カスタムカラーテーマの作成

/* app/globals.css */
@layer base {
  :root {
    /* カスタムカラーパレット */
    --primary: 259 94% 51%;           /* 紫 */
    --primary-foreground: 0 0% 100%;  /* 白 */
    
    --secondary: 199 89% 48%;         /* 青 */
    --secondary-foreground: 0 0% 100%;
    
    --accent: 339 90% 51%;            /* ピンク */
    --accent-foreground: 0 0% 100%;
    
    --success: 142 71% 45%;           /* 緑 */
    --warning: 45 93% 47%;            /* オレンジ */
    --error: 0 84% 60%;               /* 赤 */
  }
  
  .dark {
    /* ダークモード用のカラー */
    --primary: 263 70% 65%;
    --secondary: 199 70% 65%;
    --accent: 339 70% 65%;
  }
}

高度な使い方:フォームとバリデーション

shadcn/uiは、React Hook FormとZodと組み合わせて、型安全なフォームを作成できます。

React Hook Formとは? React Hook Formは、Reactでフォームを扱うためのライブラリです。以下の特徴があります:

  • 非制御コンポーネント(Uncontrolled Components)ベースで高パフォーマンス
  • 再レンダリングを最小限に抑える
  • バリデーション機能が組み込まれている
  • TypeScriptとの相性が良い

Zodとは? Zodは、TypeScriptファーストのスキーマ宣言・バリデーションライブラリです:

  • 型定義とバリデーションルールを同時に定義
  • 実行時の型チェックが可能
  • エラーメッセージのカスタマイズが簡単

バリデーションスキーマとは? バリデーションスキーマとは、データが満たすべき条件(ルール)を定義したものです。例えば:

  • メールアドレスの形式が正しいか
  • パスワードが8文字以上か
  • 年齢が18歳以上か

これらのルールをコードで表現したものがバリデーションスキーマです。

// 必要なパッケージをインストール
// npm install react-hook-form zod @hookform/resolvers

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"
import { toast } from "@/components/ui/use-toast"

// バリデーションスキーマの定義
const formSchema = z.object({
  username: z.string()
    .min(2, { message: "ユーザー名は2文字以上である必要があります" })
    .max(50, { message: "ユーザー名は50文字以下である必要があります" }),
  email: z.string()
    .email({ message: "有効なメールアドレスを入力してください" }),
  age: z.number()
    .min(18, { message: "18歳以上である必要があります" })
    .max(120, { message: "年齢が正しくありません" }),
})

export function ProfileForm() {
  // フォームの初期化
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
      age: 18,
    },
  })

  // フォーム送信処理
  function onSubmit(values: z.infer<typeof formSchema>) {
    toast({
      title: "フォームが送信されました",
      description: (
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
          <code className="text-white">
            {JSON.stringify(values, null, 2)}
          </code>
        </pre>
      ),
    })
  }

  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="john_doe" {...field} />
              </FormControl>
              <FormDescription>
                公開プロフィールに表示される名前です
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>メールアドレス</FormLabel>
              <FormControl>
                <Input type="email" placeholder="example@email.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="age"
          render={({ field }) => (
            <FormItem>
              <FormLabel>年齢</FormLabel>
              <FormControl>
                <Input 
                  type="number" 
                  {...field} 
                  onChange={(e) => field.onChange(parseInt(e.target.value))}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit">送信</Button>
      </form>
    </Form>
  )
}

AI時代の開発:v0.devとの連携

Vercel社が開発した「v0.dev」は、shadcn/uiを使用したUI生成に特化したAIツールです。

v0.devとは? v0.devは、自然言語(普通の文章)でUIの要望を伝えると、それに応じたReactコンポーネントを自動生成するAIサービスです。特徴:

  • shadcn/uiのコンポーネントを使用
  • Tailwind CSSでスタイリング
  • TypeScript対応
  • レスポンシブデザイン対応

料金について

  • 無料プラン:月10回まで生成可能
  • 有料プラン($20/月):無制限の生成、プライベートプロジェクト

v0.devの使い方

  1. v0.devにアクセス
  2. 作りたいUIを自然言語で説明
  3. AIが生成したコードをコピー
  4. プロジェクトにペースト

例:プロンプト

"価格プランを表示するカード3つ並べたセクションを作って。
Basic、Pro、Enterpriseの3つのプランで、それぞれ価格と機能リストを含む"

生成されるコード例:

export function PricingCards() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6">
      <Card>
        <CardHeader>
          <CardTitle>Basic</CardTitle>
          <CardDescription>個人利用に最適</CardDescription>
        </CardHeader>
        <CardContent>
          <p className="text-3xl font-bold">¥0</p>
          <p className="text-sm text-gray-500">永久無料</p>
          <ul className="mt-4 space-y-2">
            <li>✓ 基本機能</li>
            <li>✓ 5GBストレージ</li>
            <li>✓ メールサポート</li>
          </ul>
        </CardContent>
        <CardFooter>
          <Button className="w-full">始める</Button>
        </CardFooter>
      </Card>
      {/* Pro、Enterpriseプランも同様 */}
    </div>
  )
}

shadcn/uiを選ぶべきケース vs 選ばないべきケース

shadcn/uiが適している場合

カスタマイズ性を重視する場合

  • 独自のデザインシステムを構築したい
  • ブランドガイドラインに厳密に従う必要がある

パフォーマンスを重視する場合

  • バンドルサイズを最小限に抑えたい
  • 不要な機能を含めたくない

長期的なメンテナンスを考慮する場合

  • ライブラリの更新に依存したくない
  • コードの完全な制御権を持ちたい

従来のUIライブラリの長期メンテナンス問題: Material-UIなどの外部ライブラリを使用していると、以下のような問題が発生することがあります:

  • 破壊的変更: メジャーバージョンアップで既存コードが動かなくなる(Material-UI v4→v5でAPIが大幅変更)
  • 依存関係の競合: 他のライブラリとの依存関係で更新できない
  • サポート終了: 古いバージョンのセキュリティアップデートが提供されなくなる
  • カスタマイズの破損: ライブラリ更新でカスタマイズ部分が動作しなくなる

shadcn/uiならコードを所有しているため、これらの問題を回避できます。

Tailwind CSSを既に使用している場合

  • 既存のTailwindプロジェクトとの親和性が高い

shadcn/uiが適さない場合

すぐに使えるデザインが欲しい場合

  • Material DesignやAnt Designのような完成されたデザインシステムが必要
  • デザインにこだわりがない

Tailwind CSSを使いたくない場合

  • CSS-in-JSを好む
  • 既存のCSSフレームワークを使用している

チーム全員がTailwindに慣れていない場合

  • 学習コストを考慮する必要がある

よくある質問と回答

Q1: shadcn/uiは無料ですか?

A: はい、完全に無料でオープンソースです。MITライセンスで提供されています。

MITライセンスとは? MITライセンスは最も制限の少ないオープンソースライセンスの1つです:

  • 商用利用OK: 企業のプロダクトでも無料で使用可能
  • 改変OK: 自由に修正・カスタマイズ可能
  • 再配布OK: 修正したコードを公開・販売も可能
  • 著作権表示のみ必要: ライセンス文と著作権表示を含めるだけ
  • 保証なし: 作者は一切の責任を負わない

つまり、ほぼ何でも自由にできるライセンスです。

Q2: React以外でも使えますか?

A: 現在はReact専用です。ただし、Vue版の「shadcn-vue」やSvelte版の開発も進んでいます。

Q3: 既存のプロジェクトに導入できますか?

A: はい、可能です。ただし、Tailwind CSSの設定が必要です。

なぜTailwind CSSの設定が必要なのか? shadcn/uiのコンポーネントは、Tailwind CSSのクラス名を使ってスタイリングされているためです。例えば:

  • bg-primary → 背景色の指定
  • text-sm → 文字サイズの指定
  • hover:bg-primary/90 → ホバー時のスタイル

これらのクラスが動作するには、Tailwind CSSがプロジェクトに設定されている必要があります。

すでにTailwind CSSを使っているプロジェクトの場合:

# Tailwind CSSがすでに設定されている場合は、shadcn/uiの初期化のみでOK
npx shadcn-ui@latest init

Tailwind CSSが未導入の場合:

# Tailwind CSSのインストールと設定
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p  # tailwind.config.jsとpostcss.config.jsを生成

# その後、shadcn/uiを初期化
npx shadcn-ui@latest init

Q4: コンポーネントの更新はどうすればいいですか?

A: shadcn/uiはコピー&ペースト方式なので、自動更新はありません。必要に応じて手動で最新版をコピーするか、gitで差分を確認して更新します。

Q5: TypeScriptは必須ですか?

A: 必須ではありませんが、強く推奨されています。型安全性のメリットが大きいためです。

パフォーマンス比較

バンドルサイズの実測値

// 実際のプロジェクトでの計測結果
{
  "Material-UI (全体)": "約300KB (gzip: 90KB)",
  "Ant Design (全体)": "約380KB (gzip: 120KB)",
  "Chakra UI (全体)": "約210KB (gzip: 70KB)",
  "shadcn/ui (Button + Card + Dialog)": "約15KB (gzip: 5KB)"
}

Lighthouse スコアの比較

Lighthouseとは? Lighthouseは、Googleが提供するWebページの品質測定ツールです。Chrome DevToolsに組み込まれており、以下の項目を0-100点で評価します(高いほど良い):

  • Performance(パフォーマンス): ページの読み込み速度

    • First Contentful Paint(最初のコンテンツ表示時間)
    • Largest Contentful Paint(最大コンテンツ表示時間)
    • Total Blocking Time(メインスレッドのブロック時間)
    • Speed Index(ページ表示速度)
  • Accessibility(アクセシビリティ): 障害者への配慮

    • 適切なARIA属性の使用
    • 十分なカラーコントラスト
    • キーボード操作への対応
    • スクリーンリーダーへの対応
  • Best Practices(ベストプラクティス): Web標準への準拠

    • HTTPS使用
    • 安全なJavaScriptライブラリ
    • 適切な画像形式
    • コンソールエラーの有無
  • SEO(検索エンジン最適化): 検索エンジンへの最適化

    • メタタグの設定
    • 構造化データ
    • モバイル対応
    • クロール可能性
// 同じレイアウトを各UIライブラリで実装した場合
{
  "shadcn/ui": {
    "Performance": 98,     // 軽量なため高速
    "Accessibility": 100,  // Radix UIによる完璧な対応
    "Best Practices": 100, // 最新のWeb標準に準拠
    "SEO": 100            // 軽量で高速なため評価が高い
  },
  "Material-UI": {
    "Performance": 89,     // バンドルサイズが大きいため若干低下
    "Accessibility": 98,   // 高いが一部カスタマイズで問題が出ることも
    "Best Practices": 100,
    "SEO": 100
  }
}

実践的なTips集

Tip 1: エイリアスの設定

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/ui/*": ["./src/components/ui/*"]
    }
  }
}

Tip 2: カスタムフックの作成

// hooks/use-theme.ts
import { useEffect, useState } from 'react'

export function useTheme() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  useEffect(() => {
    const stored = localStorage.getItem('theme') as 'light' | 'dark'
    if (stored) {
      setTheme(stored)
      document.documentElement.classList.toggle('dark', stored === 'dark')
    }
  }, [])

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light'
    setTheme(newTheme)
    localStorage.setItem('theme', newTheme)
    document.documentElement.classList.toggle('dark')
  }

  return { theme, toggleTheme }
}

Tip 3: コンポーネントの組み合わせパターン

// components/composite/DataTable.tsx
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useState } from "react"

interface DataTableProps<T> {
  data: T[]
  columns: {
    key: keyof T
    label: string
    sortable?: boolean
  }[]
}

export function DataTable<T>({ data, columns }: DataTableProps<T>) {
  const [filter, setFilter] = useState('')
  const [sortKey, setSortKey] = useState<keyof T | null>(null)
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')

  const filteredData = data.filter(item =>
    Object.values(item as any).some(value =>
      String(value).toLowerCase().includes(filter.toLowerCase())
    )
  )

  const sortedData = sortKey
    ? [...filteredData].sort((a, b) => {
        const aVal = a[sortKey] as any
        const bVal = b[sortKey] as any
        const order = sortOrder === 'asc' ? 1 : -1
        return aVal > bVal ? order : -order
      })
    : filteredData

  return (
    <div className="space-y-4">
      <Input
        placeholder="検索..."
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        className="max-w-sm"
      />
      
      <Table>
        <TableCaption>データテーブル</TableCaption>
        <TableHeader>
          <TableRow>
            {columns.map(column => (
              <TableHead key={String(column.key)}>
                {column.sortable ? (
                  <Button
                    variant="ghost"
                    onClick={() => {
                      if (sortKey === column.key) {
                        setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
                      } else {
                        setSortKey(column.key)
                        setSortOrder('asc')
                      }
                    }}
                  >
                    {column.label}
                    {sortKey === column.key && (
                      <span className="ml-2">
                        {sortOrder === 'asc' ? '↑' : '↓'}
                      </span>
                    )}
                  </Button>
                ) : (
                  column.label
                )}
              </TableHead>
            ))}
          </TableRow>
        </TableHeader>
        <TableBody>
          {sortedData.map((item, index) => (
            <TableRow key={index}>
              {columns.map(column => (
                <TableCell key={String(column.key)}>
                  {String(item[column.key])}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}

まとめ:shadcn/uiがもたらす新しい開発体験

shadcn/uiは、従来のUIライブラリとは全く異なるアプローチで、開発者に次のような価値を提供しています:

🎯 主なメリット

  1. 完全な制御権 - コードを直接所有し、自由にカスタマイズ
  2. 最小限のバンドルサイズ - 必要なものだけを含める
  3. アクセシビリティ - Radix UIによる堅牢な実装
    • すべてのコンポーネントがキーボード操作可能
    • スクリーンリーダー対応(NVDA、JAWS、VoiceOver)
    • WAI-ARIA仕様に準拠
    • フォーカス管理の自動化
  4. 型安全性 - TypeScriptファーストな設計
  5. モダンな開発体験 - Tailwind CSSとの完璧な統合

🚀 これからの展望

  • AI(v0.dev)との統合がさらに進化
  • Vue、Svelte版の充実
  • コンポーネントの種類がさらに増加
  • エンタープライズ向け機能の充実

💡 導入を検討すべきタイミング

  • 新規プロジェクトの立ち上げ時
  • デザインシステムの刷新時
  • パフォーマンス改善が必要な時
  • Tailwind CSSへの移行を検討している時

shadcn/uiは「ライブラリ」ではなく「哲学」です。コンポーネントを「使う」のではなく「所有する」という考え方は、長期的なプロジェクトの保守性と拡張性を大きく向上させます。

2025年のフロントエンド開発において、shadcn/uiは単なるトレンドではなく、新しいスタンダードになりつつあります。この記事を参考に、ぜひあなたのプロジェクトでもshadcn/uiを試してみてください。

参考リンク


Happy Coding with shadcn/ui! 🎨✨