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

Claude Codeでshadcn/uiを使いこなす実践ガイド

Claude Codeでshadcn/uiを導入し、Button/Card/Form/Dialog/Table、テーマ、Playwright確認まで実装する実践ガイド。

Claude Codeでshadcn/uiを使いこなす実践ガイド

shadcn/uiは「npmから完成品を呼び出すUIライブラリ」ではありません。CLIでButtonやDialogの実装ファイルを自分のリポジトリへコピーし、Tailwind CSSとRadix UIを土台にして育てていく、React向けのコンポーネント集です。

ここにClaude Codeを組み合わせると、導入、差分確認、デザイントークン整理、フォーム実装、Playwrightでの見た目確認までを一つの開発ループにできます。ただし、Claude Codeに丸投げすると「似たButtonが3つ増える」「公式ドキュメントの古い断片を貼る」「Dialogのアクセシビリティを壊す」といった失敗も起きます。

この記事では、初心者がVite + React + TypeScriptの小さな管理画面を作る想定で、shadcn/uiをClaude Codeと安全に使う手順をまとめます。Claude Codeの基本操作に不安がある場合は、先にClaude Code入門ガイドを確認してください。

まず全体像を押さえる

公式ドキュメントは必ず一次情報を見ます。shadcn/uiのVite手順はshadcn@latestでの初期化とコンポーネント追加を案内しており、Tailwind CSS v4は@themeでテーマ変数を定義するCSS中心の考え方になっています。Dialogなどの挙動はRadix UI Primitivesが支え、PlaywrightはtoHaveScreenshot()で画面差分を検知できます。

参考リンクは作業前にClaude Codeへ渡すと、古い記事から拾った設定を混ぜるリスクが下がります。

作業の流れは次の通りです。

flowchart LR
  A["Claude Codeで既存構成を読む"] --> B["shadcn/uiをinitする"]
  B --> C["Button/Card/Form/Dialog/Tableを追加"]
  C --> D["デザイントークンをCSSへ集約"]
  D --> E["アプリ用ラッパーで再利用"]
  E --> F["Playwrightで見た目を固定"]

インストールと初期化

ここではViteを例にします。Next.jsでも考え方は同じですが、最初はルーティングやServer Componentの差を持ち込まないほうが理解しやすいです。

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: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

TypeScript側にも同じエイリアスを入れます。

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

Claude Codeには、いきなり「全部作って」ではなく、確認してから実行する形で頼みます。

このVite + React + TypeScriptプロジェクトにshadcn/uiを導入したいです。
まず package.json、vite.config.ts、tsconfig*.json、src/index.css を読んで、
不足している設定だけを箇条書きで教えてください。
その後、私が承認したら shadcn@latest init と必要なコンポーネント追加を行ってください。

確認後、CLIを実行します。

pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card field input label dialog table
pnpm add react-hook-form zod @hookform/resolvers

現在のshadcn/uiのフォーム例は、React Hook FormとZodを使い、Fieldコンポーネントでラベル、説明、エラー表示を組み立てる流れです。古い記事にあるFormField前提の断片をそのまま貼らず、公式のFormsセクションを確認してから合わせます。

ButtonとCardで小さく始める

最初に作るべきなのは、凝ったダッシュボード全体ではなく、ButtonとCardだけで完結する小さな状態表示です。Claude Codeへ「既存のcomponents/uiを直接増やさず、アプリ固有の部品はsrc/components/appに置く」と指示すると、コピー貼り付けの増殖を防ぎやすくなります。

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

type ProjectSummaryCardProps = {
  name: string
  openIssues: number
  onCreateTask: () => void
}

export function ProjectSummaryCard({
  name,
  openIssues,
  onCreateTask,
}: ProjectSummaryCardProps) {
  return (
    <Card className="max-w-md">
      <CardHeader>
        <CardTitle>{name}</CardTitle>
        <CardDescription>
          未対応のUI課題を確認し、次の作業へ進みます。
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p className="text-3xl font-semibold">{openIssues}</p>
        <p className="text-muted-foreground text-sm">open issues</p>
      </CardContent>
      <CardFooter>
        <Button onClick={onCreateTask}>タスクを追加</Button>
      </CardFooter>
    </Card>
  )
}

この段階でClaude Codeにレビューさせるなら、次のように範囲を絞ります。

src/components/app/ProjectSummaryCard.tsx だけをレビューしてください。
確認観点は props の型、shadcn/ui のimport、Tailwindクラスの読みやすさ、アクセシビリティです。
別ファイルの変更提案は必要な場合だけ理由付きで出してください。

FormはField、React Hook Form、Zodで作る

フォームは初心者がつまずきやすい部分です。見た目だけならInputを並べれば完成に見えますが、実運用ではバリデーション、エラー文、aria-invalid、送信中の無効化、初期値のリセットが必要です。

次の例は、shadcn/uiのFieldInputButtonを使った問い合わせフォームです。Controllerを使うと、React Hook Formの状態とUIコンポーネントを明示的につなげられます。

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const contactSchema = z.object({
  email: z.string().email("メールアドレスの形式で入力してください"),
  topic: z.string().min(4, "4文字以上で入力してください"),
})

type ContactFormValues = z.infer<typeof contactSchema>

export function ContactForm() {
  const form = useForm<ContactFormValues>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      email: "",
      topic: "",
    },
  })

  function onSubmit(values: ContactFormValues) {
    console.log("submit", values)
  }

  return (
    <form className="max-w-md space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
      <FieldGroup>
        <Controller
          name="email"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>メール</FieldLabel>
              <Input
                {...field}
                id={field.name}
                type="email"
                aria-invalid={fieldState.invalid}
                autoComplete="email"
              />
              <FieldDescription>返信先のメールアドレスです。</FieldDescription>
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />

        <Controller
          name="topic"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>相談内容</FieldLabel>
              <Input
                {...field}
                id={field.name}
                aria-invalid={fieldState.invalid}
                placeholder="例: 管理画面のUI改善"
              />
              {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
            </Field>
          )}
        />
      </FieldGroup>

      <Button type="submit" disabled={form.formState.isSubmitting}>
        送信する
      </Button>
    </form>
  )
}

Claude Codeにフォームを書かせるときは「Zodスキーマ、フォーム状態、UIを同じファイルに置くか分離するか」を先に決めます。小さいフォームは同じファイルで十分です。入力項目が増えたら、schema.tsContactForm.tsxactions.tsのように分けるとレビューしやすくなります。

DialogとTableを実運用に近づける

Dialogは見た目以上に難しいコンポーネントです。Radix UIのDialogは、モーダル時のフォーカストラップ、Escapeキーでのクローズ、Title/Descriptionによるスクリーンリーダー向け通知を扱います。shadcn/uiはその上にスタイル付きの部品を置くので、DialogTitleを消したり、閉じるボタンをキーボード操作できない要素に変えたりしないようにします。

次の例は、Tableの行から編集Dialogを開く管理画面の最小構成です。

import { useState } from "react"

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

const customers: Customer[] = [
  { id: "cus_001", name: "Aoi Tanaka", plan: "Pro" },
  { id: "cus_002", name: "Mika Sato", plan: "Team" },
]

export function CustomerTable() {
  const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)

  return (
    <>
      <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={() => setSelectedCustomer(customer)}
                >
                  編集
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>

      <Dialog
        open={selectedCustomer !== null}
        onOpenChange={(open) => {
          if (!open) setSelectedCustomer(null)
        }}
      >
        <DialogContent>
          <DialogHeader>
            <DialogTitle>顧客情報を編集</DialogTitle>
            <DialogDescription>
              {selectedCustomer?.name} の契約プランを確認します。
            </DialogDescription>
          </DialogHeader>
          <p className="text-sm">
            現在のプラン: <strong>{selectedCustomer?.plan}</strong>
          </p>
          <DialogFooter>
            <Button variant="outline" onClick={() => setSelectedCustomer(null)}>
              閉じる
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}

この例ではデータを固定配列にしていますが、APIから取得する場合も最初はTableの表示責務を薄く保つのがおすすめです。ページ側でfetchし、TableにはcustomersonEditだけを渡すと、Claude Codeが状態管理を過剰に増やすのを防げます。

デザイントークンとTailwind設定を整理する

デザイントークンとは、色、余白、角丸、影、フォントなど、UIの見た目を決める値に名前を付けたものです。Tailwind v4では@themeでテーマ変数を定義すると、ユーティリティクラスにも反映できます。一方、shadcn/uiの生成コードは--background--primaryのようなCSS変数を参照します。

src/index.cssは、次のように「Tailwindの読み込み」「shadcn/uiの色変数」「アプリ固有の追加トークン」を分けておくと読みやすくなります。

@import "tailwindcss";

@theme {
  --font-sans: Inter, "Noto Sans JP", system-ui, sans-serif;
  --radius-card: 0.75rem;
  --color-brand-500: oklch(0.62 0.18 250);
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
}

既存プロジェクトがTailwind v3のtailwind.config.tsを使っているなら、Claude Codeへ「v4へ移行する提案」と「v3のまま最小修正する提案」を分けて出させます。両方を同時に混ぜると、content設定、CSS変数、プラグインが重複して原因追跡が難しくなります。

コピペのズレを防ぐ運用

shadcn/uiはコードが手元に入るので自由ですが、その自由さがそのままズレになります。半年後にButtonのvariantが3系統になっているチームは珍しくありません。

私が実際に小さな管理画面で試して安定した運用は、次の4点です。

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

Claude Codeへの指示も運用ルール化します。

既存の src/components/ui はshadcn/ui由来です。
ButtonやCardを新規作成せず、既存importを使ってください。
アプリ固有の表示は src/components/app に作成してください。
変更後、重複コンポーネントが増えていないか rg "function .*Button|export .*Button" src/components を実行して確認してください。

差分確認は手元で必ず行います。

git diff -- src/components/ui
git diff -- src/components/app
pnpm lint
pnpm build

Claude Codeの生産性Tipsでも触れていますが、AIに任せる範囲を狭めるほど、レビュー時間は短くなります。特にUIは「動く」だけでは不十分で、余白、フォーカスリング、エラー表示、ダークモードまで見なければ公開品質になりません。

Playwrightで見た目を固定する

Playwrightのビジュアル比較は、初回に基準画像を作り、次回以降の差分を検知する仕組みです。OSやフォントで差が出るため、基準画像はCIと同じ環境で作るのが理想です。

pnpm create playwright
pnpm playwright install
pnpm dev
pnpm playwright test

ダッシュボードのCard、Dialog、Tableをまとめて確認するテスト例です。

import { expect, test } from "@playwright/test"

test("customer table dialog visual state", async ({ page }) => {
  await page.goto("/customers")
  await page.getByRole("button", { name: "編集" }).first().click()

  await expect(page).toHaveScreenshot("customers-dialog.png", {
    maxDiffPixels: 120,
  })
})

スナップショットを更新するときは、デザイン変更の意図をPR本文に書きます。

pnpm playwright test --update-snapshots

Claude Codeには、失敗した画像差分を見せてから修正させると精度が上がります。

Playwrightのcustomers-dialog.pngが失敗しました。
差分はDialogのFooterがモバイル幅で折り返してButton同士が近すぎる点です。
src/components/app/CustomerTable.tsx と関連CSSだけを読んで、レイアウトを修正してください。
アクセシビリティ属性とDialogTitle/DialogDescriptionは削除しないでください。

使いどころは3つある

1つ目は、管理画面の初期構築です。Button、Card、Table、Dialogが揃うと、一覧、詳細、編集、確認の基本導線を短時間で作れます。Claude Codeには「まず静的データでUIを作り、次にAPI接続」と段階を分けて頼むと安全です。

2つ目は、既存プロダクトのUI統一です。古いCSS Modulesや手書きButtonが混在している画面で、shadcn/uiの低レイヤーへ寄せていきます。いきなり全画面を置き換えるのではなく、検索フォーム、設定Dialog、空状態Cardのような小さな単位から始めます。

3つ目は、提案資料や検証用プロトタイプです。Claude Codeに「社内デモ用なのでAPIはmockでよい。ただしコンポーネント構成は本番移行を想定」と伝えると、見た目だけの捨てコードになりにくくなります。

さらに、アクセシビリティ改善にも向いています。Dialog、Label、Field、Keyboard操作の確認は、Claude Codeでアクセシビリティ対応を効率化する方法と組み合わせるとレビュー観点を増やせます。

よくある失敗と落とし穴

最も多い失敗は、公式ドキュメントを読まずに古いtailwind.config.tsの断片を貼ることです。Tailwind v4のプロジェクトにv3前提の設定を足すと、効かないクラスや重複トークンが出ます。移行中のプロジェクトでは、まずバージョンとビルド方式を確認します。

次に多いのは、components/uiをアプリ固有のロジックで汚すことです。Buttonに「決済プラン用variant」や「管理者だけ表示」の条件を入れると、全画面へ影響します。低レイヤーは汎用に保ち、事業固有の判断はcomponents/appへ逃がします。

Dialogでは、DialogTitleを消して見た目を整える失敗があります。タイトルを画面上で見せたくない場合でも、スクリーンリーダー向けに残す方法を検討します。Radix UIのドキュメントにも、TitleとDescriptionが通知管理に関わることが説明されています。

フォームでは、HTMLのrequiredだけで済ませると、サーバーエラー、非同期検証、エラー文の翻訳に対応しづらくなります。小さなフォームでも、Zodスキーマとエラー表示の場所を最初から決めておくほうが後で楽です。

Playwrightでは、ローカルの見た目だけでスナップショットを作るとCIで落ちることがあります。フォント、OS、ブラウザ、ビューポートを揃え、動的な時刻やランダムIDを固定してから比較します。

Claude Codeへの依頼テンプレート

最後に、実務でそのまま使える依頼文を置いておきます。

目的: Vite + React + TypeScriptの管理画面にshadcn/uiを導入し、
Button/Card/Form/Dialog/Tableを使った顧客一覧画面を作る。

制約:
- 公式ドキュメントの現在の手順に合わせる
- src/components/ui はshadcn/ui由来の部品だけにする
- アプリ固有部品は src/components/app に置く
- フォームはReact Hook Form + Zod + Fieldで実装する
- DialogTitle/DialogDescriptionを削除しない
- Tailwind v4の@themeとCSS変数を混ぜて使う場合は役割を説明する
- 最後にPlaywrightのtoHaveScreenshotテストを1本追加する

進め方:
1. まず既存構成を読んで不足点を報告
2. 変更計画を出す
3. 承認後に実装
4. pnpm build と pnpm playwright test を実行
5. 変更ファイルと残課題をまとめる

このテンプレートを使うと、Claude Codeが「新しいUIを作る」だけでなく、「既存の設計に合わせて壊さず進める」方向に寄ります。

まとめ

shadcn/uiとClaude Codeの相性は良いです。理由は、shadcn/uiがコンポーネントの実装をリポジトリ内に置くため、Claude Codeが差分を読み、修正し、テストまでつなげやすいからです。

一方で、自由に編集できるからこそ、導入手順、デザイントークン、配置ルール、Playwright確認を最初に決める必要があります。ButtonとCardで小さく始め、FieldベースのForm、Radix由来のDialog、Tableの責務分離へ進めると、初心者でも破綻しにくいです。

チームで同じ品質を保ちたい場合は、この記事の依頼テンプレートをCLAUDE.mdやプロジェクトの開発規約に入れてください。ClaudeCodeLabでは、こうしたUI実装ルール、レビュー観点、Claude Code依頼文をまとめた有料テンプレート集も用意しています。個人開発なら1画面、チームなら1スプリント分のレビュー時間を減らす目的で使うのが現実的です。

この記事で紹介した内容を実際に試した結果、Viteの最小プロジェクトではButton/Card/Field/Dialog/Tableの追加、@themeでのブランド色追加、Dialogを開いた状態のPlaywrightスクリーンショット確認までを一連の作業として再現できました。最も修正が必要だったのはフォームのエラー表示で、Claude Codeにaria-invalidFieldErrorを明示した後は差分レビューがかなり楽になりました。

#Claude Code #shadcn/ui #React #Tailwind CSS #UIライブラリ
無料

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

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

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

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

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

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

PR

関連書籍・参考図書

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

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