2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】shadcn/ui 入門 — 「所有する」という思想から導入・AI駆動開発活・注意点まで

2
Last updated at Posted at 2026-06-05

はじめに

React でアプリを作るとき、UI 部品をどう揃えるかは悩ましいテーマのひとつです。Material UI や Chakra UI のような npm でインストールして使うタイプのコンポーネントライブラリは便利ですが、細かくカスタマイズしようとすると theme の設定やコンポーネント内部の props と格闘することになりがちです。

そんな中で、近年フロントエンド界隈で広く使われているのが shadcn/ui です。shadcn/ui はそもそも「コンポーネントライブラリ」ではなく、従来のライブラリとは設計思想が異なるアプローチを取っています。

この記事では、shadcn/ui とは何か、どういう経緯で生まれたのか、React + Vite + TypeScript での基本的な使い方を解説します。あわせて、AI 駆動開発と特に相性が良い理由と「コードを所有する」という設計ゆえの注意点についても記載します。

shadcn/ui とは

shadcn/ui を一言で表すと、コピー&ペーストして自由にカスタマイズできる、コンポーネントのコレクション(ひな形集)です。

Material UI や Chakra UI は npm パッケージとしてインストールし、node_modules の中にあるコンポーネントを import して使います。コンポーネントの実装は自分のコードベースの外側にあり、props やテーマ設定を通じて間接的に振る舞いを変えます。

一方 shadcn/ui は、CLI を使ってコンポーネントのソースコードそのものを自分のプロジェクトにコピーします。button.tsx のようなファイルが components/ui/ 配下に生成されます。

shadcn/ui を構成する技術要素

shadcn/ui を構成する2大要素が「Radix UI」と「Tailwind CSS」です。
shadcn/ui は、「複雑な機能は Radix UI に任せ、デザインは Tailwind CSS のクラスとして自分のプロジェクトに直接コードをコピーする」という手法をとることで、高い機能性と自由なカスタマイズ性を両立させています。

Radix UI

アクセシビリティやキーボード操作、ダイアログの開閉状態管理といった「挙動(ロジック)」を提供するを担うヘッドレス(スタイルなし)のライブラリです。

Tailwind CSS

ユーティリティクラス(flex や bg-blue-500 など)を組み合わせて高速にデザインを構築できるCSSフレームワークです。

誕生の経緯と「所有」という思想

shadcn/ui は、開発者の shadcn(Vercel 所属)氏が個人プロジェクトとして公開したものが始まりです。

従来のコンポーネントライブラリには、便利な反面、構造的な悩みがありました。

  • カスタマイズしたいのに、ライブラリの抽象化の壁に阻まれる
  • 使っていないコンポーネントまでバンドルに含まれて肥大化する
  • ライブラリのバージョンアップで破壊的変更に振り回される
  • 結局「ライブラリのお作法」を覚える学習コストが発生する

shadcn/ui はこれらを 「コードの所有(ownership)」 という発想で解決しました。コンポーネントが自分のコードベースに存在するので、ボタンのホバー効果を変えたければ button.tsx の Tailwind クラスを直接書き換えるだけです。ブラックボックスは存在せず、必要なコンポーネントだけを追加するのでバンドルも無駄に膨らみません。

開発環境

ハンズオンで使う環境は以下のとおりです。記事執筆時点(2026年6月)の最新版を利用しています。

  • OS:Windows11
  • エディタ:VSCode
  • Node.js 24.16.0
  • pnpm 11.5.1
  • React 19.2.6
  • TypeScript 6.0.2
  • Vite 8.0.12
  • Tailwind CSS 4.3.0
  • shadcn CLI 4.x

shadcn は 2026年3月に CLI v4 をリリースし、Next.js だけでなく Vite・TanStack Start・React Router・Astro・Laravel のプロジェクトテンプレート生成に対応しました1。v4 で AI エージェント向けの機能も大きく強化されています。

React + Vite + TypeScript での導入手順

ここからは実際に Vite プロジェクトへ shadcn/ui を導入してみます。

手順1. Vite プロジェクトの作成

まずは Vite で React + TypeScript のプロジェクトを作成します。

pnpm create vite@latest shadcn-sample --template react-ts

手順2. Tailwind CSS の導入

shadcn/ui は Tailwind CSS を前提とするので、先にセットアップします。Tailwind v4 では Vite 用プラグインを使うのがシンプルです。

pnpm add tailwindcss @tailwindcss/vite

src/index.css の先頭に1行追加します。

src/index.css
@import "tailwindcss";

次に、追加した @tailwindcss/vite プラグインを vite.config.ts の plugins に登録します。

vite.config.ts
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Tailwind v4 では PostCSS や tailwind.config.js の設定は不要になりました。

手順3. パスエイリアスの設定

shadcn/ui は @/components のようなパスエイリアスを使ってコンポーネントを参照します。TypeScript と Vite の両方に @ を ./src として解決させる設定が必要です。

まず tsconfig.json にエイリアスを追加します。

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

エディタが補完できるよう tsconfig.app.json にも同じ設定を追加します。

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

次に Vite が @ を解決できるよう vite.config.ts を更新します。

vite.config.ts
import path from "path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

tsconfig.json と tsconfig.app.json の両方に paths を書くのを忘れないようにしてください。片方だけだとエディタの補完が効かなかったり、ビルド時にパスが解決できずエラーになったりします。

手順4. shadcn の初期化

init コマンドを実行すると、ベースカラーなどを対話形式で聞かれます。回答すると components.json が生成され、プロジェクトの設定が整います。

pnpm dlx shadcn@latest init
component.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "radix-nova",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "rtl": false,
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "menuColor": "default",
  "menuAccent": "subtle",
  "registries": {}
}

手順5. コンポーネントの追加

あとは使いたいコンポーネントを add するだけです。試しに Button を追加してみます。

pnpm dlx shadcn@latest add button

これで src/components/ui/button.tsx が生成されます。このファイルは自由に編集できます。

components/ui/button.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default:
          "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
        lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
        icon: "size-8",
        "icon-xs":
          "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
        "icon-lg": "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Button({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean
  }) {
  const Comp = asChild ? Slot.Root : "button"

  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }

src/App.tsx
import { Button } from "@/components/ui/button";

function App() {
  return (
    <div className="flex min-h-svh flex-col items-center justify-center">
      <Button>Click me</Button>
    </div>
  );
}

export default App;

pnpm dev で起動すれば、スタイルの当たったボタンが表示されます。

image.png

おまけ:CLI v4 のテンプレート

実は CLI v4 では、空のプロジェクトを用意しなくても init だけで Vite テンプレートを丸ごと生成できるようになりました1。

pnpm dlx shadcn@latest init
Select a template ›
❯ Next.js
  Vite
  TanStack Start
  React Router
  Astro
  Laravel

ここで Vite を選ぶと、Tailwind の設定やパスエイリアス、ダークモードまで含めた状態でプロジェクトが立ち上がります。新規プロジェクトならこちらの方が早いので、用途に応じて使い分けるとよいと思います。

AI 駆動開発で shadcn/ui が優れている理由

Claude Code などの AI エージェントを使った開発において、shadcn/ui は他のライブラリにない強みを持っています。

1. コードがコンテキストに乗る

AI エージェントは、自分のコードベースにあるファイルを直接読んで理解し、編集できます。shadcn/ui のコンポーネントは node_modules の奥深くではなく src/components/ui/ にプレーンなソースとして存在するため、エージェントがそのまま読み取って改変できます。

従来のライブラリだと、エージェントは props やテーマ API の仕様を「知識として」正しく覚えている必要があり、バージョン差異で誤った API を生成しがちです。shadcn/ui なら、目の前にある実コードを根拠に修正を組み立てられます。

2. Tailwind との相性

スタイルが Tailwind のユーティリティクラスとしてコンポーネント内に直接書かれているため、「ボタンを少し大きく、角丸を強めに」といった指示に対して、AI は該当する className を書き換えるだけで対応できます。CSS-in-JS のテーマオブジェクトを辿る必要がなく、変更箇所が局所的です。

3. shadcn/skills によるエージェント支援

CLI v4 で導入された shadcn/skills は、AI エージェントに対して「コンポーネントやレジストリを正しく扱うための文脈」を提供する仕組みです1。Radix UI と Base UI のプリミティブ、最新の API、コンポーネントのパターン、レジストリのワークフローをカバーしており、エージェントが shadcn CLI を「いつ・どのフラグで」呼ぶべきかまで理解します。

結果として、エージェントが誤ったコードを生成する頻度が下がり、設計システムに沿った出力が得られやすくなります。

4. CLI がエージェントに文脈を渡す

v4 では CLI コマンド自体がエージェント向けの情報源になっています1。

  • shadcn docs [component]:コンポーネントのドキュメントリンクと公式コードスニペットをレジストリから直接取得する
  • shadcn info:フレームワーク・バージョン・CSS 変数・導入済みコンポーネント・各コンポーネントのドキュメントの場所までを一覧表示する

これらは人間にも便利ですが、特に AI エージェントに対して「いま自分が作業しているプロジェクトの正確な状態」を渡すために設計されています。

まとめると

「ライブラリではない」という設計思想が、結果として AI 駆動開発との高い親和性を生んでいるのが面白いところだと思います。

注意点

ここまで利点を中心に書いてきましたが、「コードを所有する」という設計はトレードオフでもあります。導入前に知っておきたい注意点を整理します。

1. アップデートが自動では降ってこない

最大の注意点はこれだと思います。
コンポーネントが自分のコードベースにコピーされている以上、shadcn 側で公式コンポーネントが改善・修正されても、pnpm update のように自動では反映されません。
新しい実装を取り込みたい場合は再度 add し直すことになりますが、その際にローカルで加えたカスタマイズと衝突する可能性があります。

2. コードが自分のリポジトリに増える

10個のコンポーネントを追加すれば、10個分のソースが components/ui/ に並びます。便利な反面、自分が直接書いていないコードのメンテナンス責任も自分たちに移ります。
「使うコンポーネントだけ追加するのでバンドルが膨らまない」のは利点ですが、裏を返すと「追加したコンポーネントのコードは全部自分の管理対象になる」ということでもあります。

3. Tailwind CSS と Radix UI への依存

shadcn/ui は Tailwind CSS を前提としています。すでに別の CSS 設計(CSS Modules や styled-components など)で固めているプロジェクトに後から載せるのは、相性の面で難しいことがあります。
また振る舞いの土台は Radix UI(や Base UI)に依存しているため、それらのプリミティブの仕様や制約はそのまま引き継ぎます。完全にゼロから自由、というわけではありません。

4. チーム内で実装がばらつきやすい

コードを直接編集できる自由度の高さは、裏返すと「人によって同じコンポーネントの書き換え方が変わる」リスクにもなります。

5. 設計理解が必要

props を渡すだけで完結する従来のライブラリと違い、生成されたコードの中身(cva によるバリアント定義や cn の役割など)をある程度理解していないと、意図した改変ができません。

まとめ

この記事では、shadcn/ui について概要・導入手順・AI 駆動開発との相性・注意点を見てきました。

shadcn/ui は、ソースコードを自分のプロジェクトにコピーして所有するという発想のコンポーネント集です。挙動を担う Radix UI とデザインを担う Tailwind CSS の組み合わせで、機能性とカスタマイズ性を両立しています。React + Vite + TypeScript へは Tailwind 設定・パスエイリアス・init・add の流れで導入でき、CLI v4 のテンプレートを使えばさらに手軽です。
コードが自分のコードベースに存在するため、AI エージェントがそのまま読み取って編集できます。これが AI 駆動開発との相性の良さの核心で、v4 の shadcn/skills や docs / info コマンドがそれを後押しします。
一方で、アップデートが自動では降ってこない、メンテナンス責任が自分に移る、実装がばらつきうるなど、所有と引き換えのトレードオフもあります。これらを理解した上で付き合うのが活かすコツだと思います。

参考

  1. shadcn/cli v4(2026年3月)で、複数フレームワークのテンプレート生成、shadcn/skills、docs / info コマンドなどが追加されました。 ↩ ↩2 ↩3 ↩4

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?