はじめに
この記事は、shadcn/ui でデザインシステムを構築するための導入編です。
以下の理論編で「なぜ shadcn/ui がデザインシステムに向いているのか」を整理しました。
この記事では実際にプロジェクトを立ち上げ、トークンを設計し、コンポーネントをプロジェクト仕様に拡張するところまでを手を動かしながら進めます。
また各ステップでは、その設計・実装が Claude Code などのAIエージェントの出力にどう効くかも、一言ずつ添えていきます。整えたデザインシステムは、人間だけでなくエージェントにとっての手本にもなります。
開発環境
開発環境は以下の通りです。
- OS:Windows 11
- エディタ:VSCode
- Node.js 24.16.0
- pnpm 11.5.2
- React 19.2.7
- TypeScript 6.0.3
- Vite 8.0.16
- Tailwind CSS 4.3.0
- shadcn CLI 4.x
ステップ1. Vite プロジェクトの作成
まずは Vite で React + TypeScript のプロジェクトを作成します。
pnpm create vite@latest shadcn-design-system --template react-ts
cd shadcn-design-system
pnpm install
ステップ2. Tailwind CSS の導入
shadcn/ui は Tailwind を前提とするので、先にセットアップします。Tailwind v4 では Vite 用プラグインを使うのがシンプルです。
pnpm add tailwindcss @tailwindcss/vite
src/index.css の先頭に1行追加します。
@import "tailwindcss";
vite.config.ts の plugins にプラグインを登録します。
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
ステップ3. パスエイリアスの設定
shadcn/ui は @/components のようなパスエイリアスを使うので、TypeScript と Vite の両方に @ を ./src として解決させます。
まず tsconfig.json です。Vite の react-ts テンプレートは tsconfig.json が参照(references)だけを持つ構成なので、compilerOptions.paths をここに足します。
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
Vite の react-ts テンプレートでは型チェックを tsconfig.app.json が担当します。ここに paths がないと tsc がエイリアスを解決できずビルドが落ちるので、同じ設定を追加します。
{
"compilerOptions": {
// ...
"paths": {
"@/*": ["./src/*"]
}
}
}
次に Vite が @ を解決できるよう 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"),
},
},
});
ステップ4. shadcn の初期化
init コマンドを実行すると、ベースカラーなどを対話形式で聞かれます。回答すると components.json が生成されます。
pnpm dlx shadcn@latest init
components.json はプロジェクトの設定ファイルです。デザインシステムの観点で特に重要なのは tailwind.cssVariables で、これが true になっていることを確認してください(init のデフォルトは true です)。CSS変数によるテーマ管理が有効になり、トークンを一元管理する前提が整います。
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
init の時点で、src/index.css に shadcn/ui のトークン群(--background、--primary など)と、Tailwind へ橋渡しする @theme inline ブロックが書き込まれます。ここからが本題のデザインシステム構築です。
ステップ5. トークンを設計する
ここがデザインシステム構築の核心です。コンポーネントを追加する前に、まず トークンをどう設計し、どう名付けるか を決めておくと、後の作業が一貫します。
トークンの3つのカテゴリ
トークンは、おおまかに色・角丸・タイポグラフィの3つで考えると整理しやすいです。
| カテゴリ | 例 | 一元管理する場所 |
|---|---|---|
| 色(color) |
--primary、--background、--brand
|
:root / .dark
|
| 角丸(radius) | --radius |
:root |
| タイポグラフィ(font) |
--font-sans、--font-mono
|
@theme |
命名規則の決め方:役割で名付ける
トークンの命名で大事なのは、色そのものではなく役割で名付ける ことです。--purple-600 ではなく --primary や --brand のように、「何のための色か」で名前を付けます。
役割ベースにしておくと、例えば後でブランドカラーを紫から青に変えても、トークン名はそのままで値だけ変えれば済みます。色名で付けてしまうと、--purple-600 という名前なのに中身が青、という不整合が起きます。
shadcn/ui が採用している background / foreground のペア規約(面の色とその上の文字色をセットで定義する)に乗っかるのが、最も楽で一貫性が出ます。独自トークンを足すときも、この規約に合わせて --brand と --brand-foreground のように対で定義するのがよいと思います。
AIエージェントへの効き目
役割ベースの命名は、エージェントにとっての語彙になります。「ブランド色のボタンを足して」と指示したとき、--purple-600 のような色名トークンしか無いと、エージェントは色を直書きするか、近い名前を当てずっぽうで選びがちです。--brand という役割名があれば、エージェントは bg-brand を選べます。命名そのものが、エージェントの選択肢を正しい方向へ絞り込みます。
ブランドカラーを足したいとき、既存の --primary を流用したくなりますが、これはたいてい破綻します。--primary は主にプライマリボタンなどの強調要素のための色なので、ブランドの色をボタン以外にも広く使い回そうとすると、ボタンの色を変えた途端に無関係な箇所まで巻き込まれます。役割が違う色は、専用トークンとして新しく定義するのが正解だと思います。
デザインツールのトークンと同期する
Figma などでデザイントークンが定義されている場合は、その「プライマリカラー」「角丸ルール」「フォント」を、これから書く src/index.css のトークンに落とし込みます。デザイン側とコード側で同じ役割名を使っておくと、変更時の対応漏れが減ります。
ステップ6. トークンを実装する
設計が決まったら、src/index.css にトークンを書きます。Tailwind v4 では色を OKLCH 形式で定義します。OKLCH は人間の知覚に近い色空間で、明度を揃えたまま色相だけ変えるといった調整がしやすいです。
ファイルの全体像
init が生成する src/index.css は、おおまかに次の3ブロックで構成されています。
-
@importと@custom-variant(Tailwind 本体の読み込みとダークモードの定義) -
@theme inline(CSS変数を Tailwind のユーティリティに接続する) -
:root/.dark(トークンの実際の値)
この記事では、init が生成したファイルに独自のブランドトークンを追記する形で進めます。
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-destructive: var(--destructive);
/* 独自トークンを追記 */
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
/* radius は --radius から導出する */
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
}
: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);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--destructive: oklch(0.577 0.245 27.325);
/* 独自のブランドカラーを追加(役割で命名) */
--brand: oklch(0.55 0.22 295);
--brand-foreground: oklch(0.985 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);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--destructive: oklch(0.704 0.191 22.216);
--brand: oklch(0.7 0.18 295);
--brand-foreground: oklch(0.205 0 0);
}
ダークモードは、同じトークン名を .dark 内で別の値に上書きするだけです。コンポーネント側のクラスは一切変わりません。
@custom-variant dark (&:is(.dark *)); の1行は、init が生成してくれます。これがあることで .dark クラスを親に持つ要素へダークモードのトークンが効きます。
@theme inline でユーティリティに接続する
@theme inline は、定義したCSS変数を Tailwind のユーティリティクラス(bg-primary や bg-brand 等)として使えるようにするためのブロックです。init が基本のトークンは登録してくれているので、独自に足したトークンを追記します。
上の例では、--brand という設計値に対して --color-brand: var(--brand); を @theme inline に書きました。これで --brand と bg-brand というユーティリティが結びつきます。以降、トークンを変えたいときは :root の値だけ触れば、参照しているすべての箇所に反映されます。
AIエージェントへの効き目
@theme inline でトークンとユーティリティを結びつけておくと、エージェントが bg-brand のようなクラスを「実在するもの」として安全に使えます。逆にここを登録し忘れると、エージェントが bg-brand を生成してもスタイルが当たらず、原因の分かりにくいバグになります。トークンを足したら必ずここに追記する、という対応関係そのものが、エージェントの出力を機能する状態に保ちます。
ステップ7. コンポーネントを追加する
トークンの土台ができたので、コンポーネントを追加します。試しに Button を入れます。
pnpm dlx shadcn@latest add button
src/components/ui/button.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 で起動すれば、トークンの色が当たったボタンが表示されます。
ステップ8. コンポーネントをプロジェクト仕様に拡張する
shadcn/ui のコンポーネントは cva(class-variance-authority)でバリアントを定義しています。ここに、ステップ5〜6で用意した独自トークンを使うバリアントを足します。
components/ui/button.tsx を開いて編集します。
const buttonVariants = cva(
"inline-flex items-center justify-center ...", // 共通スタイル
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
// ↓ デザインシステム独自のバリアントを追加する
brand: "bg-brand text-brand-foreground hover:bg-brand/90",
},
},
}
)
ここで重要なのは、独自バリアントでも色を直書きせず、bg-brand のようにトークンを参照することです。
// ❌ Before: 色を直書きすると、変更時に全箇所を追う必要がある
<button className="bg-[#7c3aed] text-white">購入する</button>
// ✅ After: トークンを参照すれば、index.css の一行で全体が変わる
<Button variant="brand">購入する</Button>
Before は一見動きますが、同じ紫がコード中に散らばり、ブランドカラーを変えたときに全置換が必要になります。After はトークンに集約されているため、src/index.css の --brand 一行で全体が変わります。使わないバリアントは削除して構いません。
AIエージェントへの効き目
ここが、エージェントの出力に最も効くポイントだと思います。「購入ボタンを目立たせて」のような指示に対し、エージェントは手元のコードを手本にします。既存のバリアントがすべてトークンを参照していれば、エージェントもそれに倣って variant="brand" を使うか、トークンを参照する新バリアントを足します。逆に Before のような直書きが1つでも混じっていると、エージェントはそれを「このプロジェクトの流儀」と学習し、直書きを再生産します。整ったバリアント定義そのものが、レビューで毎回指摘する代わりのガードレールになります。
ステップ9. 動作確認
ここまでの設定が本当に機能するか、実際にビルドして確認します。
pnpm build
ビルドが通ったら、--brand トークンが Tailwind のユーティリティに正しく変換されているかを、生成されたCSSで確認できます。@theme inline に追記した --color-brand を経由して、次のようなルールが出力されます。
.bg-brand { background-color: var(--brand); }
.bg-brand\/90:hover { background-color: color-mix(in oklab, var(--brand) 90%, transparent); }
--brand の値(oklch(0.55 0.22 295))が :root に出力され、bg-brand がそれを参照する形になっていれば、トークンとユーティリティの接続は成功です。
実際に Button のバリアントを並べたサンプル画面を作って描画したものが以下です。brand(紫)・default・secondary・outline・destructive の各バリアントを置いています。
ライトモード
ダークモード(<html class="dark"> を付けただけ)
コンポーネント側のクラスは一切変えず、.dark を付けるだけで全トークンが切り替わり、独自の --brand も含めてダークモード用の値に入れ替わっているのが確認できます。これがトークンベースのテーマ管理の効果です。
ステップ10. AIエージェントに skills を渡す
ここまでで作ったトークンやバリアントを、Claude Code などのAIエージェントにも正しく認識させたいところです。そのための公式の仕組みが shadcn/skills です。
skills は、AIアシスタントに shadcn/ui のプロジェクトを認識した文脈を与えます。導入すると、アシスタントはそのプロジェクトに合った正しいAPIとパターンで、コンポーネントを見つけ・追加し・組み合わせ・カスタマイズできるようになります。
skills を導入する
skills は skills エコシステム経由で導入します。
pnpm dlx skills add shadcn/ui
これでプロジェクトに shadcn skill が入り、shadcn/ui を扱う際にエージェントが自動で読み込みます。一度入れれば有効になります。
skill が持つ知識
skill がエージェントに与える知識は、ドキュメント上、おおまかに次の5つです。
| 内容 | 概要 |
|---|---|
| プロジェクトコンテキスト | 毎回のやり取りで shadcn info --json を実行し、フレームワーク・Tailwindのバージョン・エイリアス・ベースライブラリ(radix / base)・アイコンライブラリ・導入済みコンポーネント・解決済みパスを取得する |
| CLIコマンド |
init / add / search / view / docs / diff / info / build の全コマンドのリファレンス(フラグ、dry-run、マージ、プリセット、テンプレート含む) |
| テーミングとカスタマイズ | CSS変数・OKLCH・ダークモード・カスタムカラー・border radius・バリアントの扱い方(Tailwind v3 / v4 両対応) |
| レジストリ作成 |
registry.json の書き方や公開方法 |
| MCPサーバー | shadcn MCP サーバーのセットアップと、レジストリからの検索・追加 |
ここまでのステップで整えたトークンやバリアントは、この「プロジェクトコンテキスト」と「テーミング」の知識を通じてエージェントに伝わります。
動作の仕組み
skill は、おおよそ次の流れで働きます。
-
プロジェクト検出 —
components.jsonを見つけると有効化される -
コンテキスト注入 —
shadcn info --jsonの結果をエージェントのコンテキストに注入する -
パターン強制 — フォームに
FieldGroup、選択肢にToggleGroup、セマンティックカラーの使用といった shadcn/ui の構成ルールに従わせる -
コンポーネント発見 — コードを書く前に
shadcn docs/searchや MCP で該当コンポーネントのドキュメントを引く
skill は「プロジェクトの実状態(info)」と「shadcn の正しい使い方の知識」を毎回エージェントに渡してくれます。ステップ5〜8で整えたトークンやバリアントが整然としているほど、info で渡る情報も一貫し、エージェントの出力が設計システムに沿いやすくなります。
これで、ステップ5〜8で整えたトークンとコンポーネントが、人間、エージェントともに「正しい手本」として機能する状態になります。
ステップ11. チーム・複数プロジェクトで配布する
ここまでで、1つのプロジェクト内でデザインシステムが回る状態になりました。最後に、これを複数人・複数プロジェクトに広げる方法です。
開発メンバーは共通コンポーネントを使う
カスタマイズ済みのコンポーネントは、node_modules ではなく @/components/ui/button のようにプロジェクト内のパスからインポートして使ってもらいます。これで全員が同じトークン・同じバリアントを共有できます。
レジストリで配布する
複数プロジェクトでデザインシステムを共有したい場合は、shadcn のレジストリ機能が使えます。独自のトークンやコンポーネントを registry として定義し、CLI で各プロジェクトに配布できます。テーマ変数の追加・上書きも registry の cssVars で表現できるので、トークンごと配布できます。
公式リポジトリのアップデートへの対応
コードを所有する以上、公式リポジトリのコンポートがアップデートされても自動では降ってきません。
add --overwrite で最新実装に上書きできますが、自分が加えたカスタムコードは消えるため、Git の差分を見ながら手動でマージすることになります。差分だけ先に見たい場合は add --diff で確認できます。
まとめ
この記事では、Vite + Tailwind v4 のプロジェクトに shadcn/ui を導入し、トークンの設計・命名から、コンポーネントの拡張・配布までを記載しました。
コンポーネントを追加する前にトークンを役割ベースで設計しておくと、後がずっと楽になります。bg-brand のようにトークンを参照する習慣さえ作れば、デザインの変更は index.css の一行で完結し、統一感が仕組みとして保たれます。そしてこの一貫性は、人間だけでなくAIエージェントにとっての手本にもなり、skills と合わせて「設計システムに沿ったコードが自然に出てくる」状態を作れます。

