Use Cases (更新: 2026/6/7)

shadcn/uiは「インストールする部品」じゃない。コードを自分のものにする使い方

shadcn/uiの使い方を、npx shadcn addでの追加、Radix UI+Tailwindの構成、コピーしたコードのカスタマイズまで。Claude Codeと組む手順を僕の失敗込みで解説。

shadcn/uiは「インストールする部品」じゃない。コードを自分のものにする使い方

初めてshadcn/uiを触ったとき、僕は盛大に勘違いしていました。

npm install で入れて、<Button> を呼ぶ。よくあるUIライブラリと同じだろう、と。ところが追加したButtonは node_modules のどこにもない。自分のリポジトリの src/components/ui/button.tsx に、ふつうのReactコードとして置いてあったんです。

最初は「えっ、これメンテどうするの」と不安でした。でも数日使って分かりました。これは部品を借りるライブラリじゃない。部品をもらって、自分のコードにする仕組みなんだと。

このズレを理解しないままClaude Codeに丸投げすると、事故ります。「似たButtonが3つ」「公式の古い断片を貼る」「Dialogのアクセシビリティを壊す」。僕が全部やりました。今日はその反省を込めて、shadcn/uiの正しい使い方と、Claude Codeと安全に組む手順を書きます。

Claude Code自体に不安がある人は、先にClaude Code入門:インストールから最初の30分で成果を出す始め方を読んでおくと、この記事が一気に楽になります。

この記事の要点

  • shadcn/uiは「インストールして呼ぶ」UIライブラリではなく、CLIでコードを自分のリポジトリにコピーして育てるコンポーネント集。
  • 中身は Radix UI(挙動・アクセシビリティ)+ Tailwind CSS(見た目) の組み合わせ。だから自由に書き換えられる。
  • コンポーネントの追加は npx shadcn@latest add button のように1コマンド。node_modules ではなく src/components/ui/ に実ファイルが置かれる。
  • カスタマイズの基本は「components/ui は触りすぎない/アプリ固有の都合は components/app に逃がす」。これを破ると半年後に崩壊する。
  • Claude Codeとは相性がいい。コードが手元にあるので、AIが差分を読んで直してテストするループに乗せやすい。

shadcn/uiは「ライブラリ」じゃない、という話

ふつうのUIライブラリ(Material UIなど)は、完成品を node_modules から呼び出します。便利だけど、ボタンの角丸を2px変えたいだけで !important の沼にハマったり、ライブラリのアップデートで見た目が勝手に変わったりする。借り物だから、最後の細部が自分の手に入らないんですね。

shadcn/uiは発想が逆です。CLIを叩くと、コンポーネントのソースコードそのものがあなたのプロジェクトに書き込まれます。Buttonが欲しければButtonの .tsx ファイルがコピーされてくる。あとはそれを、自分が書いたコードと同じように好き勝手に編集していい。

この違いをひとことで言うと、こうです。

ふつうのUIライブラリshadcn/ui
コードの置き場所node_modules(借り物)自分のリポジトリ(自分のもの)
カスタマイズpropsやテーマ変数の範囲内ファイルを直接書き換え放題
アップデートバージョン更新で一括自分で取り込む(勝手に変わらない)
中身の正体独自実装Radix UI + Tailwind CSS

「アップデートが手動なんて面倒」と思うかもしれません。でも逆に言うと、あなたが何も触っていないのに見た目が壊れる事故が起きないということです。デザインを長く運用するなら、これは効きます。

中身はRadix UIとTailwind CSSでできている

shadcn/uiのコードを開くと、必ず2つの登場人物が出てきます。

ひとつが Radix UI。これは「見た目のないUI部品」を提供するライブラリです。たとえばDialog(モーダル)には、開いている間はその外をクリックできなくする、Escapeキーで閉じる、フォーカスを中に閉じ込める、スクリーンリーダーにタイトルを読ませる、といった地味だけど面倒な挙動が必要です。Radixはこの「動き」だけを担当します。見た目はゼロ。

もうひとつが Tailwind CSSclassName="px-4 py-2 rounded-md" のように、クラス名で見た目を当てるCSSの仕組みです。shadcn/uiは、Radixの「動き」にTailwindの「見た目」を塗って、ちょうどいい既製品にしてあるわけです。

この構造を知っていると、カスタマイズの勘所が一発で分かります。

  • 挙動を変えたい(閉じる条件、フォーカスの動き)→ Radixのpropsをいじる
  • 見た目を変えたい(色、余白、角丸)→ Tailwindのクラスを書き換える

逆に、これを知らずに「Dialogが閉じない」とTailwind側を延々いじっても直りません。僕はこれで半日溶かしました。動きはRadix、見た目はTailwind。これだけは覚えて帰ってください。

Tailwind側の設計そのものをもっと深掘りしたい人は、Claude CodeでTailwind CSSを使いこなす実践Tips:崩れにくいUI改善ガイドが役に立ちます。

まず動かす:プロジェクト作成から最初の追加まで

説明より手を動かしたほうが早いです。Vite + React + TypeScriptの小さな管理画面を想定して進めます。Next.jsでも考え方は同じですが、最初はルーティングやServer Componentの差を持ち込まないほうが理解しやすいです。

まずプロジェクトを作って、Tailwind v4を入れます。

pnpm create vite@latest shadcn-claude-demo -- --template react-ts
cd shadcn-claude-demo
pnpm install
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node

vite.config.ts にTailwindのViteプラグインと @ エイリアスを設定します。shadcn/uiの生成コードは @/components/ui/button のようなimportを使うので、ここを曖昧にすると後で全部つまずきます。

import path from "node:path"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      // shadcn/uiは "@/components/ui/..." でimportする。ここを実体のsrcに向ける
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

TypeScript側にも同じエイリアスを入れておきます(tsconfig.json)。

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

ここまで来たら初期化です。init がプロジェクトを見て components.json(shadcn/uiの設定ファイル)を作り、必要なCSS変数やユーティリティを用意してくれます。

# npm派ならこちら
npx shadcn@latest init

# pnpm派ならこちら(やっていることは同じ)
pnpm dlx shadcn@latest init

そして本題、コンポーネントの追加です。ここが「shadcn コンポーネント 追加」で検索してたどり着く人がいちばん知りたいところだと思います。やることは1コマンドです。

# Buttonを1つだけ追加する
npx shadcn@latest add button

# 複数まとめても良い(管理画面で使う一式)
npx shadcn@latest add card field input label dialog table

これを叩くと、src/components/ui/button.tsx などの実ファイルが生えてきます。node_modules ではありません。試しに開いてみてください。中身はただのReactコンポーネントで、Radixのimportと、Tailwindのクラスを組み立てる関数(cva など)が並んでいるはずです。これがあなたのコードになった瞬間です。

フォームも使うので、関連パッケージも入れておきます。

pnpm add react-hook-form zod @hookform/resolvers

コピーしたコードを「自分のもの」にする

ここからがshadcn/uiの本番です。せっかくコードが手元に来たので、書き換えましょう。

一番わかりやすいのが variant(見た目のバリエーション)の追加です。たとえば「危険な操作用」の赤いボタンが欲しいとします。Material UIなら「そんなvariantは無い」で終わりですが、shadcn/uiならButtonのコードを開いて、自分で足せます。

src/components/ui/button.tsx の中にある、variantを定義している部分にこう追記します。

// src/components/ui/button.tsx の variants 定義に1行足すだけ
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",
        outline: "border border-input bg-background hover:bg-accent",
        // ここから追記:自社用の「危険操作」バリアント
        danger:
          "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-400",
      },
      // ...size などはそのまま
    },
  }
)

これで <Button variant="danger">削除する</Button> が使えます。ライブラリの中を書き換えているのではなく、自分のファイルを編集しているだけ。これがshadcn/uiの気持ちよさです。

ただし注意。components/ui の中身を「アプリの都合」で汚すのは別問題です。色や角丸のような見た目のバリエーション追加はOK。でも「決済プランがProのときだけ表示」みたいな事業ロジックを混ぜるのはNG。後者は全画面に影響するので、必ずアプリ側のラッパーに逃がします。

色やトークンを体系立てて管理したくなったら、デザイントークン設計の始め方:CSS変数とStyle Dictionaryでダークモードまで一本化で、--primary のようなCSS変数を一本化する考え方をまとめています。

コピペで動く:CardとDialogの最小例

ここまでで追加したButton・Card・Dialog・Tableを組んで、「行の編集ボタンを押すとモーダルが開く」管理画面の最小構成を作ります。そのまま貼って動くコードです(固定データなのでAPIは不要)。

import { useState } from "react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

type Customer = {
  id: string
  name: string
  plan: "Free" | "Pro" | "Team"
}

// 本来はAPI取得。最初は固定データでUIだけ完成させるのがおすすめ
const customers: Customer[] = [
  { id: "cus_001", name: "Aoi Tanaka", plan: "Pro" },
  { id: "cus_002", name: "Mika Sato", plan: "Team" },
]

export function CustomerPanel() {
  // null のときDialogは閉じ、Customerが入るとそのお客さんで開く
  const [selected, setSelected] = useState<Customer | null>(null)

  return (
    <Card className="max-w-2xl">
      <CardHeader>
        <CardTitle>顧客一覧</CardTitle>
      </CardHeader>
      <CardContent>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>顧客名</TableHead>
              <TableHead>プラン</TableHead>
              <TableHead className="text-right">操作</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {customers.map((customer) => (
              <TableRow key={customer.id}>
                <TableCell className="font-medium">{customer.name}</TableCell>
                <TableCell>{customer.plan}</TableCell>
                <TableCell className="text-right">
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => setSelected(customer)}
                  >
                    編集
                  </Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </CardContent>

      {/* Radixが開閉・フォーカス・Escapeを面倒みてくれる */}
      <Dialog
        open={selected !== null}
        onOpenChange={(open) => {
          if (!open) setSelected(null)
        }}
      >
        <DialogContent>
          <DialogHeader>
            {/* DialogTitle/Descriptionはスクリーンリーダー用。消さない */}
            <DialogTitle>顧客情報を編集</DialogTitle>
            <DialogDescription>
              {selected?.name} の契約プランを確認します。
            </DialogDescription>
          </DialogHeader>
          <p className="text-sm">
            現在のプラン: <strong>{selected?.plan}</strong>
          </p>
          <DialogFooter>
            <Button variant="outline" onClick={() => setSelected(null)}>
              閉じる
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </Card>
  )
}

ポイントはコメントにも書いた DialogTitleDialogDescription です。画面に見せたくなくても、スクリーンリーダー利用者には必要な情報なので消さない。Radixが「タイトルが無いよ」と警告を出すのもこのためです。アクセシビリティをまとめて固めたい人は、Claude Codeでアクセシビリティ対応を実装する実践ワークフローもあわせてどうぞ。

Claude Codeに任せるときの頼み方

shadcn/uiはコードが手元にあるので、Claude Codeとの相性が抜群です。AIが差分を読んで、直して、テストするループに素直に乗ります。ただし任せ方を間違えると、冒頭の「似たButtonが3つ」が現実になります。

僕がやらかした失敗を、対策とセットで3つ。

1つ目、丸投げして部品を増殖させた。 「いい感じのボタン作って」と言ったら、既存のButtonを無視して新しい MyButton を勝手に作りました。今は最初に範囲を固定します。

既存の src/components/ui はshadcn/ui由来です。
ButtonやCardを新規作成せず、既存のimportを使ってください。
アプリ固有の表示は src/components/app に作ってください。
作業後、似た部品が増えていないか確認してから報告してください。

2つ目、公式の古い断片を貼られた。 Claude Codeはたまに、古い記事から拾った tailwind.config.ts 前提の設定を混ぜてきます。今のTailwindはv4でCSS中心です。なので作業前に公式URLを渡して、一次情報に合わせさせます(後述のFAQに公式リンクをまとめました)。

3つ目、確認を自分の目だけに頼った。 「最後に僕がレビューすればいい」は忙しい日に必ず破綻します。今は機械で分かることは機械にやらせています。

# 重複コンポーネントが増えていないかをまず機械で確認
git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build

UIは「動く」だけでは不十分で、余白・フォーカスリング・エラー表示・ダークモードまで見て初めて公開品質です。だからこそ、レビュー観点を毎回ゼロから思い出すのではなく、依頼テンプレートに固定しておくと楽になります。フォーム周りの具体はClaude CodeでReact Hook Formを安全に実装する入門ガイドに切り出してあります。

チームで崩れないための配置ルール

shadcn/uiの自由さは、放っておくとそのまま「ズレ」になります。半年後に Button のvariantが3系統に分裂しているチームは、本当に珍しくありません。僕が小さな管理画面で試して安定したのは、たった4つのルールです。

ルールなぜ効くか
src/components/ui はshadcn/ui由来の低レイヤーだけにする公式の更新や再生成の影響を追いやすい
アプリ固有の部品は src/components/app に置く事業ロジックとUI基盤を混ぜない
components.json のaliasを勝手に変えないimportのズレ・崩壊を防ぐ
Claude Codeには変更対象ファイルを毎回明示する似た部品の新規作成を抑える

このルール自体をコンポーネントの粒度や命名と一緒に設計図に落とすなら、デザインシステムは「ボタン量産」で死ぬ:コンポーネント設計と運用を小さく育てるが下敷きになります。要は、低レイヤーは汎用に保ち、事業の都合はアプリ側に逃がす。この線引きを最初に決めるかどうかで、半年後の地獄度が変わります。

よくある質問

Q. shadcn/uiは npm install できないんですか? shadcn/ui本体を完成品として npm install するわけではありません。CLI(npx shadcn@latest add ...)が、コンポーネントの実コードをあなたの src/components/ui/ にコピーします。Radix UIやTailwindといった依存パッケージは通常どおりインストールされます。

Q. コンポーネントを追加したファイルはどこに置かれますか? 既定では src/components/ui/ です。node_modules ではなく自分のリポジトリ内なので、Gitで差分が見え、自由に編集できます。置き場所は components.json で確認・変更できます。

Q. shadcn/uiのアップデートはどうやるんですか? 完成品ライブラリのような一括更新はありません。必要なコンポーネントを add で取り直して差分を確認するか、公式のdiff情報を見て手で取り込みます。手間はありますが、勝手に見た目が壊れない利点と裏表です。

Q. Radix UIとTailwindは別で入れる必要がありますか? コンポーネントを add すると、必要なRadixのパッケージは自動で入ります。Tailwindは事前にプロジェクトへ入れておくのが前提です(この記事の手順で導入済み)。

Q. Next.jsでも使えますか? 使えます。init がNext.js構成も検出します。考え方はViteと同じですが、Server/Client Componentの境界("use client")が増えるぶん、最初はViteで感覚をつかむほうが理解しやすいです。

公式の一次情報はこのあたりを押さえておけば十分です。

実際に試した結果

冒頭の勘違いから始まった僕のshadcn/ui体験は、いまでは管理画面づくりの標準になりました。

Viteの最小プロジェクトで、Button・Card・Field・Dialog・Tableを add し、Buttonに自社用の danger variantを足し、Dialogを開いた状態まで一連で再現できます。いちばん効いたのは「components/ui は見た目だけ・事業ロジックはアプリ側」という線引きを最初に決めたことでした。これを徹底してからは、Claude Codeに任せても部品が増殖しなくなり、差分レビューが目に見えて短くなりました。

shadcn/uiは「楽に部品を呼ぶ」道具ではありません。コードを自分のものにして、長く育てる道具です。借り物だと最後の細部が手に入らない。でもこれは全部手元にある。その代わり、置き場所のルールと、Claude Codeへの頼み方だけは、最初に決めておいてください。そこさえ押さえれば、UI実装はかなり気持ちよくなります。

UI実装のレビュー観点やClaude Codeへの依頼文を、もっと体系立てて手元に置きたい人は、教材一覧にテンプレート集をまとめてあります。個人開発なら1画面、チームなら1スプリント分のレビュー時間を減らす、くらいの現実的な目的で使うのがちょうどいいです。

#Claude Code #shadcn/ui #React #Tailwind CSS #Radix UI
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。