Design System Spec
docs/DESIGN_SYSTEM.md の全文。トークン・コンポーネント・規約の包括仕様
AdXpand Design System — Component & Token Reference
関連ドキュメント(必ず併読)
ファイル 役割 CLAUDE.md最初に読む — 絶対ルール + ドキュメント索引 docs/DESIGN_SYSTEM.md(この文書)コンポーネント仕様・トークン定義 docs/BEHAVIOR.mdModal / Toast / Undo の判断ルール docs/PATTERNS.mdユースケース → 実装パターン逆引き 各第一階層の README.mddir の役割 / 本番対応 / 規約 判断フロー:
- やりたいことが
PATTERNS.mdにあるか確認- なければ
BEHAVIOR.mdで確認・通知の方針を決める- 必要なコンポーネントの仕様をこの
DESIGN_SYSTEM.mdで確認- 実装してライト/ダーク両方で確認
0. 大原則(違反厳禁)
-
Tailwind v4 トークン経由でしか色・余白を指定しない。
- 色は
bg-bg-1/text-fg-0/text-accent-text/border-borderのユーティリティクラス - 余白は
p-3(12px) /gap-2(8px) など 4px グリッド準拠 - 角丸は
rounded-mdなど。rounded-[8px]のような任意値は禁止 - フォントサイズも
text-13(デフォ本文) /text-12(キャプション) など事前定義済みトークン - 例外: 1回限りのレイアウト微調整で 絶対に トークン外が必要な場合のみ任意値(極力避ける)
- 色は
-
既存コンポーネントを使う。新規作成は最終手段。
<Btn>を使わず<button>を書くのはNG<Badge>があるので<span style={{...}}>label</span>は書かない- 新規が必要なら、まず「既存で組み合わせられないか」を考える
- どうしても必要なら cva + Tailwind + トークン で構築
-
データスロップを作らない。
- 意味のない数字・アイコン・スタッツを埋めない
- 「セクションが寂しいから何か足す」→ NG。何も足さずスペースを保つ
-
日本語を優先する。
- UIコピーは日本語(「保存」「キャンセル」「削除」)
- 英語の方が自然な固有名詞(Deploy, Build, Commit, Region)はそのまま英語
- ボタンラベルに敬語は不要(×「保存します」○「保存」)
-
アクセントカラーを乱用しない。
- ネオングリーンは「主要なアクション」と「活性状態」のみ
- 1画面にプライマリボタンは原則1つ(複数ある場合は他を secondary に落とす)
- すべてを光らせると、何も光らない
1. ファイル構成と import 規則
app/
├─ app.css ← Tailwind v4 + @theme トークン定義(必須・1つだけ)
├─ root.tsx ← import "./app.css" でロード
├─ components/
│ ├─ index.ts ← バレル
│ ├─ Btn.tsx
│ ├─ Badge.tsx
│ ├─ Input.tsx
│ ├─ Field.tsx
│ ├─ Modal.tsx
│ ├─ AlertDialog.tsx
│ ├─ Card.tsx
│ ├─ Tabs.tsx
│ ├─ Empty.tsx
│ ├─ Skel.tsx
│ ├─ Table.tsx
│ └─ Banner.tsx
└─ lib/
└─ cn.ts ← clsx + tailwind-merge ラッパー
import 例
import { Btn, Badge, Modal, Field, Input, AlertDialog } from "~/components";
import type { BtnProps } from "~/components/action-form/Btn"; // 型は個別 import 可
コンポーネント単位の CSS ファイルは作らない。すべて Tailwind utilities + cva で完結。
2. デザイントークン早見表
すべて app/app.css の @theme ブロックで定義 → Tailwind がユーティリティ自動生成。
Colors
| Token | Tailwind class | 用途 |
|---|---|---|
--color-bg-0 (#050608) | bg-bg-0 | ページ背景 |
--color-bg-1 (#0c0e11) | bg-bg-1 | カード |
--color-bg-2 (#14171c) | bg-bg-2 | 浮いた面、ホバー前の表面 |
--color-bg-3 (#1a1e23) | bg-bg-3 | ホバー |
--color-bg-4 (#22272e) | bg-bg-4 | 押下・選択中 |
--color-fg-0 (#f5f7f8) | text-fg-0 | 主要テキスト |
--color-fg-1 (#c4c8cd) | text-fg-1 | 本文 |
--color-fg-2 (#8a9199) | text-fg-2 | 補助 |
--color-fg-3 (#5b6068) | text-fg-3 | キャプション・プレースホルダー |
--color-border | border-border | 通常のボーダー |
--color-border-strong | border-border-strong | 強調ボーダー |
--color-accent (#c8ff00) | bg-accent / text-accent | 主要アクションの塗り(ライト時もネオン) |
--color-accent-text | text-accent-text / border-accent-text | アクセント色のテキスト/線(ライト時 #3e5800) |
--color-accent-fg (#0f1400) | text-accent-fg | accent 上に乗る文字色 |
--color-accent-soft | bg-accent-soft | アクセント色のセル背景(rgba 12%) |
--color-danger (#ff5a5f) | bg-danger / text-danger | エラー、破壊的操作 |
--color-warn (#ffb547) | bg-warn / text-warn | 警告 |
--color-info (#5b9eff) | bg-info / text-info | 情報通知 |
--color-success (#4ade80) | bg-success / text-success | 成功 |
使い分けルール:
- テキスト上の
bgは1段階以上明度差をつける(bg-bg-1上にtext-fg-1以上、text-fg-3は可読性が低いのでキャプションのみ) - 緑をテキスト/ボーダーで使うときは 必ず
text-accent-text/border-accent-text(ライト時に自動で暗いオリーブ #3e5800 へ切替) - 緑を塗りで使うときは
bg-accentでOK(ライト時もネオンのまま)
Spacing(4px グリッド)
Tailwind 標準を使用:p-1 (4px) / p-2 (8px) / p-3 (12px) / p-4 (16px) / p-5 (20px) / p-6 (24px) / p-8 (32px) / p-10 (40px) / p-12 (48px)。
ルール:
- カード内 padding:
p-3(コンパクト) 〜p-5(通常) - セクション間:
gap-10〜gap-12 - インライン要素間 gap:
gap-2
Radius
| Token | Tailwind | 用途 |
|---|---|---|
--radius-xs (3px) | rounded-xs | 細かい記号、tag |
--radius-sm (5px) | rounded-sm | Badge / Chip |
--radius-md (7px) | rounded-md | ボタン・入力 |
--radius-lg (10px) | rounded-lg | カード・モーダル |
--radius-xl (14px) | rounded-xl | 大きいカード |
| - | rounded-full | アバター・ドット |
Type scale
| Token | Tailwind | 用途 |
|---|---|---|
--text-10 | text-10 | 極小(数値オーバーレイ等) |
--text-11 | text-11 | キャプション |
--text-12 | text-12 | フォームラベル・補助 |
--text-13 | text-13 | 本文デフォルト |
--text-14 | text-14 | やや強調 |
--text-16 | text-16 | h3・モーダルタイトル |
--text-18 | text-18 | h2 |
--text-20 | text-20 | セクションタイトル |
--text-24 | text-24 | h1 |
--text-32 | text-32 | ページ大タイトル |
--text-48 | text-48 | display |
ルール:
- 本文は
text-13 - キャプションは
text-11またはtext-12 - モノスペースは
font-monoを数値・コード・ID・タイムスタンプに限定。装飾目的で使わない
3. コンポーネント別ルール
3.1 Button (<Btn>)
app/components/Btn.tsx 参照。
Variants:
| variant | When to use |
|---|---|
primary | 画面のメインCTA。1画面1つが原則 |
secondary | デフォルトの普通のボタン |
ghost | 透明背景。ツールバー、アイコンボタン |
danger | 削除・取消など破壊的操作の実行 |
link | インラインのリンク風 |
Sizes: sm / md (デフォ) / lg
他 props:
loading={true}でスピナー表示iconOnly={true}でアスペクト比 1:1(アイコン専用)
例:
<Btn variant="primary">
<Plus size={14} />
プロジェクトを作成
</Btn>
<Btn variant="danger" loading={isDeleting}>削除</Btn>
<Btn variant="ghost" iconOnly aria-label="設定">
<Settings size={14} />
</Btn>
DON'T:
- 同一フォームに primary を複数置かない
- 本文中のアクションに
lgを使わない(smでよい) - Modal のフッターでは「キャンセル(ghost)」+「確定(primary/danger)」の順で右寄せ
3.2 Badge
app/components/Badge.tsx 参照。
Tones: subtle / accent / success / warn / danger / info
ステータス表示用。状態を表すもの(Ready / Failed / Draft / v2.4.1)。
<Badge tone="success">live</Badge>
<Badge tone="warn">throttled</Badge>
<Badge tone="subtle">draft</Badge>
任意のラベル用途には使わない(それは普通の <span> で十分)。
3.3 Forms
必ず <Field> で包む:
<Field label="プロジェクト名" help="小文字・ハイフンのみ">
<Input placeholder="adxpand-production" />
</Field>
エラー表示:
<Field label="Email" error="有効なメールアドレスを入力してください">
<Input type="email" error />
</Field>
react-hook-form + zod の例:
const schema = z.object({
name: z.string().min(1, "プロジェクト名を入力してください").max(40),
});
type Values = z.infer<typeof schema>;
const { register, handleSubmit, formState: { errors, isSubmitting } } =
useForm<Values>({ resolver: zodResolver(schema) });
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<Field label="プロジェクト名" error={errors.name?.message}>
<Input {...register("name")} error={!!errors.name} />
</Field>
<Btn type="submit" variant="primary" loading={isSubmitting}>作成</Btn>
</form>
DO:
labelは必須。ラベルなし入力は例外的な UI(検索バーなど)のみhelpは通常時ヒント、errorは検証失敗時- エラー文言は zod スキーマに書く(コンポーネント側に書かない)
DON'T:
- プレースホルダーをラベル代わりにしない
- 必須マークは
requiredprop で。手で*を書かない
3.4 Feedback コンポーネント使い分け ⚠️ 重要
| 状況 | 使うべきコンポーネント | 理由 |
|---|---|---|
| 一時的な操作結果通知 | Toast (sonner) | 消えて良い情報(保存完了、コピーしました) |
| ページ全体に関わる永続的な通知 | Banner | 消すまで残る(メンテナンス告知、支払い失敗) |
| ブロッキングな確認 | Modal | タスクを中断させて良い時 |
| 破壊的確認(Yes/No) | AlertDialog | 削除・上書きなど取り返しがつかない時 |
| 長い編集フォーム | Drawer (TODO) | 周辺コンテキストを残したい時 |
| 軽い追加入力(1-2項目) | Popover (TODO) | Modal は重すぎる時 |
| ロード中表示(形が予測可能) | Skel | コンテンツの形を先に見せる |
| スピナー(形が予測不可) | Btn 内 loading | 進捗が分かる時は Progress |
| 空の状態 | Empty | データがない時の案内 |
混同しやすい判断:
- Toast vs Banner: Toast はユーザー操作の結果、Banner はシステム側の事情
- Modal vs Drawer: 短く終わるなら Modal、作業領域が必要なら Drawer
- Modal vs AlertDialog: 情報入力を伴うなら Modal、Y/Nだけなら AlertDialog
- Skel vs Spinner: 形が予測できるなら Skel、できないならスピナー
3.5 Modal / AlertDialog
<Modal open={open} onOpenChange={setOpen} title="メンバーを招待" size="md">
<Field label="メールアドレス">
<Input type="email" />
</Field>
<div className="flex justify-end gap-2 mt-4">
<Btn variant="ghost" onClick={() => setOpen(false)}>キャンセル</Btn>
<Btn variant="primary">招待を送信</Btn>
</div>
</Modal>
<AlertDialog
open={open}
onOpenChange={setOpen}
variant="destructive"
title="プロジェクトを削除"
desc="この操作は取り消せません。すべてのデータが削除されます。"
confirmLabel="削除する"
requireText="acme-prod"
onConfirm={async () => { await api.delete(); }}
/>
詳しくは BEHAVIOR.md。
3.6 Toast(Sonner)
import { toast } from "sonner";
toast.success("保存しました");
toast.error("通信エラー");
toast("削除しました", {
action: { label: "元に戻す", onClick: () => restore() },
duration: 8000,
});
toast.promise(api.save(), {
loading: "保存中…",
success: "保存しました",
error: "保存に失敗しました",
});
<Toaster /> は app/root.tsx に配置済み。詳しくは BEHAVIOR.md。
3.7 Tabs
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">概要</TabsTrigger>
<TabsTrigger value="members">メンバー</TabsTrigger>
<TabsTrigger value="settings">設定</TabsTrigger>
</TabsList>
<TabsContent value="overview">…</TabsContent>
<TabsContent value="members">…</TabsContent>
</Tabs>
URL 同期するなら value / onValueChange を URLSearchParams と繋ぐ。
3.8 Table
シンプルな表は AdXpand <Table> 系で十分。ソート・ページング・virtual scroll が必要になったら TanStack Table を導入。
<Table>
<THead>
<TR>
<TH>名前</TH>
<TH>ステータス</TH>
</TR>
</THead>
<tbody>
{rows.map(r => (
<TR key={r.id}>
<TD>{r.name}</TD>
<TD><Badge tone={r.status}>{r.status}</Badge></TD>
</TR>
))}
</tbody>
</Table>
3.9 Empty / Banner / Skel
<Empty
icon={<Inbox size={20} />}
title="まだプロジェクトがありません"
desc="プロジェクトを作成して始めましょう"
action={<Btn variant="primary"><Plus size={14} />作成</Btn>}
/>
<Banner tone="warn" title="支払いが失敗しました" action={<Btn size="sm">支払い情報を更新</Btn>}>
クレジットカードの有効期限を確認してください。
</Banner>
<Skel className="h-8 w-48" />
4. アイコン (lucide-react)
import { Plus, Trash2, Settings, ChevronRight } from "lucide-react";
<Plus size={14} />
<Trash2 size={14} className="text-danger" />
ルール:
- size は
12 / 14 / 16 / 20を基本 strokeWidthはデフォルト(1.5)固定classNameで色指定(text-fg-2など)- アイコンだけのボタンには必ず
aria-labelか<Tooltip>(実装後)を付ける
5. レイアウトパターン
5.1 アプリケーション構造(実装予定 app/routes/_layout.tsx)
┌─────────────────────────────────────┐
│ appnav (sidebar, 240px) │ appmain │
│ │ │
│ - ブランド │ header │
│ - ナビリンク │ ─── │
│ - フッター補助情報 │ content │
└─────────────────────────────────────┘
- ナビは
bg-bg-1 border-r border-border - メインは
overflow-auto p-6
5.2 セクション構造
<section className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-20 font-medium">タイトル</h2>
<Btn variant="primary">アクション</Btn>
</div>
<Card>{/* 中身 */}</Card>
</section>
5.3 密度(density)
現在の実装はコンパクト固定。将来的にユーザー設定として data-density="relaxed" で切替可能にする予定(@theme の制御変数で対応)。
5.4 レスポンシブ規則
ブレークポイントは Tailwind v4 標準を使用する。新規トークンは定義しない。
| 接頭辞 | 最小幅 | 想定端末 |
|---|---|---|
| (なし) | 0px | スマートフォン |
sm: | 640px | 大型スマートフォン |
md: | 768px | タブレット縦・小型ノート |
lg: | 1024px | タブレット横・標準ノート |
xl: | 1280px | デスクトップ |
モバイルファースト原則
- デフォルトはスマホ向けのスタイルを書き、
md:以上で広い画面用に上書きする - 例:
flex flex-col md:flex-row/grid-cols-1 md:grid-cols-3/p-4 md:p-8 lg:を使うのは「サイドバー2カラム」など本当に広い画面でしか成立しないレイアウトのみ
主要パターン
サイドバーナビ
md以上: 240px 固定で常時表示md未満: 隠す + 左上ハンバーガー → ドロワーでtranslate-xで出し入れ<AppShell>がこの挙動を内蔵
PageHeader
md以上: タイトルと actions をjustify-betweenで横並びmd未満: 縦に積む + actions はflex-wrap<PageHeader>側で対応済み
Table
- 常に最小幅(
min-w-[480px]程度)を保ち、overflow-x-autoで横スクロール - スマホで縦に組み直さない(情報密度が崩れる)
<Table>側で対応済み
Modal / AlertDialog
max-w-mdなどで幅指定 +w-fullで狭幅にフィット- 高さは指定しない(コンテンツに合わせる)
カードグリッド
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4- 1列 → 2列 → 3列の段階を基本にする
フォーム
- 単純なフォーム: 縦に積む(カラムを切らない)
- 設定画面など長いフォーム:
md:grid-cols-2で2列、入力欄が短い項目だけ並べる
DON'T
- ❌ ブレークポイントごとの個別文字サイズトークン(
--text-mobile-smなど)を増やす - ❌ ピクセル指定で
@media (max-width: 720px)を CSS に直書き(Tailwind 接頭辞を使う) - ❌ Container Queries(
@container)を全コンポーネントに適用する。AppShell main / Card 内のような「コンテナ幅依存で切替えたい場面」だけに限定 - ❌ モバイルでナビを下部固定タブにする(このシステムはダッシュボード前提でドロワー方式が正解)
6. アニメーション規則
Tailwind の標準 transition / animate を使用。tailwindcss-animate 相当の data-state アニメーションは Radix と連携。
推奨
- 状態変化:
transition-colors/transition-opacity(デフォ duration-150) - モーダル登場:
data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 - モーダル退場:
data-[state=closed]:animate-out data-[state=closed]:fade-out-0
DON'T
- 2秒を超えるアニメーションは避ける
- アクセント発光をリンク全部に付けない
- 1 画面に派手なアニメーションは 1-2 箇所まで
7. コピーライティング
トーン
- 説明的で簡潔。敬体(です/ます)ベース
- ボタンは命令形の短い動詞(「保存」「キャンセル」「続行」)
- 成功メッセージは完了形(「保存しました」「デプロイが完了しました」)
- エラーメッセージは何が・なぜ・どうすべきかを短く
例
| 場面 | Good | Bad |
|---|---|---|
| ボタン | 「保存」 | 「保存する」「データを保存」 |
| 成功 | 「変更を保存しました」 | 「保存しました!🎉」 |
| エラー | 「ファイルサイズが 10MB を超えています」 | 「エラー」「不正な入力」 |
| 空状態 | 「まだプロジェクトがありません」 | 「何もありません」 |
| 確認 | 「プロジェクトを削除しますか?」 | 「削除しますか?」 |
数字・単位
- 数字は半角。
2,481 件のように件の前に半角スペースを入れる - 時間は
00:42/42 秒/2 分前/1 時間前 - バイトは
1.2 MB(小数1桁、単位前にスペース)
記号
- 読点は
、、句点は。(UIでは省くことも多い) - 矢印は
→(->や>>ではない) - 中黒
·で区切る(|や/より柔らかい)
8. アンチパターン集
❌ グラデーション背景を多用
AdXpand は黒×ネオングリーンの直線的な美学。グラデーションは控える。
❌ 絵文字を UI に入れる
🎉 ✅ ❤️ は使わない。lucide-react のアイコンを使う。
❌ 派手な独自 shadow
Tailwind 標準の shadow-sm / shadow-md で十分。
❌ 左ボーダーアクセント
SaaS UI によくあるが AdXpand の美学ではない。Badge の dot や背景色で代用。
❌ primary ボタンの乱立
1画面に primary が3つある → 2つを secondary に落とす。
❌ 何でも Modal
「ちょっと確認」を毎回 Modal にしない。Popover / Banner / Toast を検討。
❌ Modal 内にさらに Modal
ネスト禁止。必要なら ステップ(Steps) に分解。
❌ Skel の乱用
空コンテナ全域を埋めない。コンテンツの形を模倣する。
❌ Badge tone の不一致
| Status | Tone |
|---|---|
| Ready / Success / Active | success |
| Failed / Error / Offline | danger |
| Warning / Pending / Throttled | warn |
| Info / Building / Queued | info |
| Draft / Archived / Disabled | subtle |
「重要」を danger で表現しない(赤は失敗)。
❌ 任意値の濫用
p-[13px]→p-3(12px) かp-3.5(14px) のどちらかに寄せるtext-[15px]→text-14かtext-16bg-[#0c0e11]→bg-bg-1
❌ アイコンだけのボタンに aria-label なし
必ず aria-label="..." か <Tooltip> を付ける。
9. ユースケース → コンポーネント索引
| やりたいこと | 使うもの |
|---|---|
| フォームを送信する | <Btn variant="primary"> |
| 破壊的な操作の確認 | <AlertDialog variant="destructive"> |
| 非ブロッキングで成功通知 | toast.success() |
| ページ全体の永続通知 | <Banner> |
| 文章中の注釈 | <Callout> |
| 長い設定フォーム | <Drawer> |
| ちょっとした追加入力 | <Popover> |
| 補足情報 (hover) | <Tooltip> |
| ステータス表示 | <Badge tone="..."> |
| 削除可能タグ | <Chip onRemove={...}> |
| 絞り込みチップ | <FilterPill> |
| 入力フィールド | <Field><Input /></Field> |
| 複数行入力 | <Field><Textarea /></Field> |
| 検索ボックス | <SearchInput> |
| キーボードショートカット | <Kbd> |
| トグルスイッチ | <Switch> |
| 単一選択 | <RadioGroup> / <Select> |
| 複数選択 | <Checkbox> |
| ロード中 (形が読める) | <Skel /> |
| ロード中 (形が読めない) | <Btn loading> 等 |
| 進捗表示 | <Progress> |
| 空の状態 | <Empty> |
| 階層ナビ | <Breadcrumb> |
| ページ送り | <Pagination> |
| マルチステップ | <Steps> |
| 折りたたみFAQ | <Accordion> |
| テーブル | <Table> |
| タブ | <Tabs> |
| カード | <Card> |
| ユーザー画像 | <Avatar> |
| グラフ | <LineChart> / <BarChart> / <Donut> |
| 一括操作バー | <BulkActionBar> |
| ページ見出し | <PageHeader> |
| 見出し | <Heading level={1|2|3|4}> |
| サイドバー | <AppShell> + <NavLink> |
| 行コンテキストメニュー | <DropdownMenu> |
未実装の追加候補: Dropzone / Upload 系 (要件が出たら追加)。
10. Claude への指示メタ
このドキュメントを読んだあなた (Claude) は以下を守ってください:
- コンポーネント選定は必ずこのドキュメントを参照する
- 新規コンポーネントを作る前に既存の組み合わせで解決できないか検討
- トークン違反(任意値の使用)をしたら自分で気づいて修正
- UIコピーは §7 に従う。長すぎる / 丁寧すぎる文はカット
- 迷ったら短く・少なく・淡々と。AdXpand はシリコンバレー SaaS の美学
Last updated: 2026-04 Version: 0.2 (Vite + RR7 + Tailwind v4 移行版) Maintainer: AdXpand Design System