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 の役割 / 本番対応 / 規約

判断フロー:

  1. やりたいことが PATTERNS.md にあるか確認
  2. なければ BEHAVIOR.md で確認・通知の方針を決める
  3. 必要なコンポーネントの仕様をこの DESIGN_SYSTEM.md で確認
  4. 実装してライト/ダーク両方で確認

0. 大原則(違反厳禁)

  1. 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回限りのレイアウト微調整で 絶対に トークン外が必要な場合のみ任意値(極力避ける)
  2. 既存コンポーネントを使う。新規作成は最終手段。

    • <Btn> を使わず <button> を書くのはNG
    • <Badge> があるので <span style={{...}}>label</span> は書かない
    • 新規が必要なら、まず「既存で組み合わせられないか」を考える
    • どうしても必要なら cva + Tailwind + トークン で構築
  3. データスロップを作らない。

    • 意味のない数字・アイコン・スタッツを埋めない
    • 「セクションが寂しいから何か足す」→ NG。何も足さずスペースを保つ
  4. 日本語を優先する。

    • UIコピーは日本語(「保存」「キャンセル」「削除」)
    • 英語の方が自然な固有名詞(Deploy, Build, Commit, Region)はそのまま英語
    • ボタンラベルに敬語は不要(×「保存します」○「保存」)
  5. アクセントカラーを乱用しない。

    • ネオングリーンは「主要なアクション」と「活性状態」のみ
    • 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

TokenTailwind 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-borderborder-border通常のボーダー
--color-border-strongborder-border-strong強調ボーダー
--color-accent (#c8ff00)bg-accent / text-accent主要アクションの塗り(ライト時もネオン)
--color-accent-texttext-accent-text / border-accent-textアクセント色のテキスト/線(ライト時 #3e5800)
--color-accent-fg (#0f1400)text-accent-fgaccent 上に乗る文字色
--color-accent-softbg-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-10gap-12
  • インライン要素間 gap: gap-2

Radius

TokenTailwind用途
--radius-xs (3px)rounded-xs細かい記号、tag
--radius-sm (5px)rounded-smBadge / Chip
--radius-md (7px)rounded-mdボタン・入力
--radius-lg (10px)rounded-lgカード・モーダル
--radius-xl (14px)rounded-xl大きいカード
-rounded-fullアバター・ドット

Type scale

TokenTailwind用途
--text-10text-10極小(数値オーバーレイ等)
--text-11text-11キャプション
--text-12text-12フォームラベル・補助
--text-13text-13本文デフォルト
--text-14text-14やや強調
--text-16text-16h3・モーダルタイトル
--text-18text-18h2
--text-20text-20セクションタイトル
--text-24text-24h1
--text-32text-32ページ大タイトル
--text-48text-48display

ルール:

  • 本文は text-13
  • キャプションは text-11 または text-12
  • モノスペースは font-mono を数値・コード・ID・タイムスタンプに限定。装飾目的で使わない

3. コンポーネント別ルール

3.1 Button (<Btn>)

app/components/Btn.tsx 参照。

Variants:

variantWhen 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:

  • プレースホルダーをラベル代わりにしない
  • 必須マークは required prop で。手で * を書かない

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 / onValueChangeURLSearchParams と繋ぐ。

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. コピーライティング

トーン

  • 説明的で簡潔。敬体(です/ます)ベース
  • ボタンは命令形の短い動詞(「保存」「キャンセル」「続行」)
  • 成功メッセージは完了形(「保存しました」「デプロイが完了しました」)
  • エラーメッセージは何が・なぜ・どうすべきかを短く

場面GoodBad
ボタン「保存」「保存する」「データを保存」
成功「変更を保存しました」「保存しました!🎉」
エラー「ファイルサイズが 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 の不一致

StatusTone
Ready / Success / Activesuccess
Failed / Error / Offlinedanger
Warning / Pending / Throttledwarn
Info / Building / Queuedinfo
Draft / Archived / Disabledsubtle

「重要」を danger で表現しない(赤は失敗)。

❌ 任意値の濫用

  • p-[13px]p-3 (12px) か p-3.5 (14px) のどちらかに寄せる
  • text-[15px]text-14text-16
  • bg-[#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) は以下を守ってください:

  1. コンポーネント選定は必ずこのドキュメントを参照する
  2. 新規コンポーネントを作る前に既存の組み合わせで解決できないか検討
  3. トークン違反(任意値の使用)をしたら自分で気づいて修正
  4. UIコピーは §7 に従う。長すぎる / 丁寧すぎる文はカット
  5. 迷ったら短く・少なく・淡々と。AdXpand はシリコンバレー SaaS の美学

Last updated: 2026-04 Version: 0.2 (Vite + RR7 + Tailwind v4 移行版) Maintainer: AdXpand Design System