ui-design-system について
UI Design System は、一貫したデザイントークン、レイアウト、アクセシビリティを必要とする画面の開発やレビュー向けのスキルです。Tailwind と shadcn/ui の指針を示し、UI コードの変更前に既存トークンを確認するよう求めます。
主な機能
- 01既存のデザイントークンを確認
- 02セマンティックな色と文字のトークンを定義
- 03一貫した余白とレイアウトを適用
- 04利用可能な場合は shadcn/ui のプリミティブを使用
- 05レスポンシブとアクセシビリティの規則に準拠
活用シーン
- 01既存のデザインシステムに沿ってUIコンポーネントを構築
- 02プロジェクトのデザイントークンを設定
- 03画面の一貫性とアクセシビリティをレビュー
- 04既存レイアウトに shadcn/ui を組み込む
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hieutrtr/ ai1-skills --skill ui-design-systemインストール後、Claude Code で/ui-design-systemを入力して読み込まれたか確認します。
同梱ファイルreferences/design-tokens-reference.md
詳細
UI デザインシステム
使用するタイミング
次の場合に、このスキルを有効にしてください:
- デザインシステムに準拠する新しい UI コンポーネントを作成するとき
- 一貫した余白と構造でページレイアウトを構築するとき
- デザイントークン(色、タイポグラフィ、余白)を設定または拡張するとき
- プロジェクトで使う色、フォント、余白の値を選ぶとき
- デザインの一貫性とアクセシビリティの観点から UI コードをレビューするとき
- shadcn/ui コンポーネントを既存のレイアウトに統合するとき
次の場合は、このスキルを使用しないでください:
- バックエンド API を実装するとき(
python-backend-expertを使用してください) - コンポーネントやフックをテストするとき(
react-testing-patternsを使用してください) - E2E ブラウザーテストを行うとき(
e2e-testingを使用してください) - デザインシステムと無関係な一般的な React パターンを扱うとき(
react-frontend-expertを使用してください) - デプロイや CI/CD を扱うとき(
deployment-pipelineを使用してください)
手順
ステップ 0:既存のデザイントークンを確認する
UI コードを生成する前に、プロジェクト内に既存のトークンがあるか確認してください:
- カスタムテーマの拡張について、
tailwind.config.ts(または.js)を読みます - CSS カスタムプロパティについて、
src/styles/globals.cssまたはapp/globals.cssを読みます - shadcn/ui が設定されている場合は、
components.jsonを読みます
デザイントークンが存在しない場合は、初期セットを生成し、作業を進める前にユーザーに確認を求めてください(「エッジケース」を参照)。
デザイントークン
カラートークン
Tailwind が使用する CSS カスタムプロパティとして色を定義します。コンポーネント内でハードコードした hex/rgb 値は使用しないでください。
CSS カスタムプロパティ(shadcn/ui との互換性のため HSL 形式):
/* globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--accent: 210 40% 96%;
--accent-foreground: 222 47% 11%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
--radius: 0.5rem;
}
.dark {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 224 76% 48%;
}
}
Tailwind の設定マッピング:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
},
},
} satisfies Config;
色の使用ルール:
- 常にセマンティックなトークンクラスを使用します:
bg-primary、text-foreground、border-border - コンポーネントコードで Tailwind の生のパレットカラー(
bg-blue-500)は使用しないでください - すべての色にダークモード用のバリアントを定義する必要があります
- 色付きの背景上のテキストには
foregroundバリアントを使用します
タイポグラフィのスケール
Tailwind のフォントサイズユーティリティを使用して、タイポグラフィのスケールを定義します:
| トークン | サイズ | 行の高さ | 用途 |
|---|---|---|---|
text-xs | 12px | 16px | キャプション、補助テキスト |
text-sm | 14px | 20px | 補足テキスト、ラベル |
text-base | 16px | 24px | 本文(デフォルト) |
text-lg | 18px | 28px | 小見出し |
text-xl | 20px | 28px | セクション見出し |
text-2xl | 24px | 32px | ページ見出し |
text-3xl | 30px | 36px | ヒーロー見出し |
タイポグラフィのルール:
tailwind.config.tsで基本フォントを設定します:fontFamily: { sans: ["Inter", "system-ui", "sans-serif"] }- 見出しとラベルには
font-medium(500)、本文にはfont-normal(400)を使用します text-2xl以上の見出しにはtracking-tightを使用します- 読みやすさのため、
max-w-prose(65ch)で行の長さを制限します
余白(8pt グリッド)
すべての余白の値は、8pt を基本とするグリッドに従います:
| Tailwind クラス | 値 | 用途 |
|---|---|---|
p-1 / gap-1 | 4px | インラインアイコンのパディング、狭い間隔 |
p-2 / gap-2 | 8px | 要素間のコンパクトな間隔 |
p-3 / gap-3 | 12px | 入力欄のパディング、小さなカードのパディング |
p-4 / gap-4 | 16px | 標準的なコンポーネントのパディング |
p-6 / gap-6 | 24px | カードのパディング、セクション間の間隔 |
p-8 / gap-8 | 32px | セクションのパディング |
p-12 / gap-12 | 48px | ページセクション間の間隔 |
p-16 / gap-16 | 64px | 大きなレイアウト間隔 |
余白のルール:
- 個別のマージンを使う代わりに、Flex/Grid の子要素には
gap-*を使用します - 要素を縦に並べる場合は、
space-y-*を優先します - カードでは、パディングに
p-6、内部要素間の間隔にgap-4を使用します - ページセクションには、垂直方向のパディングとして
py-12またはpy-16を使用します - 余白の体系を混在させないでください(
margin: 13pxのような値は使いません)
コンポーネントの構造
階層:コンテナー > レイアウト > コンテンツ
すべてのコンポーネントは、次の 3 層構造に従います:
// Container: outer wrapper with spacing, background, border
<Card className="p-6">
{/* Layout: flex/grid arrangement */}
<div className="flex items-center gap-4">
{/* Content: actual UI elements */}
<Avatar src={user.avatar} alt={user.name} />
<div className="space-y-1">
<h3 className="text-sm font-medium">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.role}</p>
</div>
</div>
</Card>
セマンティック HTML
目的に応じて適切な HTML 要素を使用します。
| 要素 | 用途 | 使用しない |
|---|---|---|
<button> | クリック可能なアクション | <div onClick> |
<a> | ナビゲーションリンク | リンクに対する <button> |
<nav> | ナビゲーション領域 | <div> |
<main> | ページの主要コンテンツ | <div> |
<article> | 独立したコンテンツ(カード、投稿) | <div> |
<section> | 見出しを伴うテーマ別のグループ | <div> |
<aside> | サイドバーや補足的なコンテンツ | <div> |
<header> | セクションの導入コンテンツ | <div> |
<footer> | セクションのフッターコンテンツ | <div> |
<ul> / <ol> | 項目のリスト | 各項目に対する <div> |
shadcn/ui プリミティブ
独自の実装より、shadcn/ui コンポーネントを優先してください。
| 必要なもの | 使用するもの | 使用しないもの |
|---|---|---|
| ボタン | <Button> | スタイルを適用した独自の <button> |
| モーダル | <Dialog> | ポータルを使用した独自のモーダル |
| ドロップダウン | <DropdownMenu> | 独自のドロップダウン |
| カード | <Card> | スタイルを適用した <div> |
| 入力欄 | <Input> | スタイルを適用した <input> |
| セレクト | <Select> | ネイティブの <select> |
| ツールチップ | <Tooltip> | 独自のツールチップ |
| タブ | <Tabs> | 独自のタブコンポーネント |
| テーブル | <Table> | 通常の <table> |
| アラート | <Alert> | 独自のバナー用 div |
shadcn/ui がインストールされていない場合は、同等のパターンと一貫したクラスの順序で、プレーンな Tailwind に切り替えてください。
TypeScript コンポーネントインターフェース
JSDoc の説明を付けた TypeScript インターフェースとして props をエクスポートします。
/** Props for the UserProfileCard component. */
interface UserProfileCardProps {
/** User data to display. */
user: User;
/** Called when the edit button is clicked. */
onEdit?: (userId: string) => void;
/** Visual variant of the card. */
variant?: "default" | "compact";
/** Additional CSS classes applied to the root element. */
className?: string;
}
export function UserProfileCard({
user,
onEdit,
variant = "default",
className,
}: UserProfileCardProps) {
// ...
}
Props のルール:
- インターフェース名は
{ComponentName}Propsにします - コンポーネントの合成のため、すべてのコンポーネントに
className?: stringを含めます - 見た目のバリエーションには、別々のコンポーネントではなく
variantprop を使用します - オプショナルな prop のデフォルト値は、インターフェースではなく分割代入で指定します
- 値を制限する文字列には、ユニオン型を使用します:
"sm" | "md" | "lg"
レスポンシブデザイン
ブレークポイント
Tailwind のモバイルファーストのブレークポイントを使用します。
| プレフィックス | 最小幅 | 対象 |
|---|---|---|
| (なし) | 0px | モバイル(デフォルト) |
sm: | 640px | 大型スマートフォン/小型タブレット |
md: | 768px | タブレット |
lg: | 1024px | デスクトップ |
xl: | 1280px | 大型デスクトップ |
レスポンシブのルール:
- モバイルファーストで設計します。ベーススタイルをモバイル向けにし、その後ブレークポイントごとに上書きを追加します
- レスポンシブグリッドには
grid-cols-1 md:grid-cols-2 lg:grid-cols-3を使用します - モバイルではナビゲーションを縦に並べます:
flex-col md:flex-row - モバイルでは重要度の低い要素を非表示にします:
hidden md:block - コンテナの最大幅を設定します:
max-w-7xl mx-auto px-4 sm:px-6 lg:px-8
レスポンシブレイアウトのパターン
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div>
</div>
アクセシビリティ(WCAG 2.1 AA)
コントラスト比
- 通常のテキスト(< 18px または < 14px 太字):最小コントラスト比 4.5:1
- 大きなテキスト(>= 18px または >= 14px 太字):最小コントラスト比 3:1
- UI コンポーネントおよびグラフィックオブジェクト:最小コントラスト比 3:1
インタラクティブ要素の要件
すべてのインタラクティブ要素に、次の要件を満たす必要があります。
- 表示されるラベルまたは aria-label:
<Button aria-label="Close dialog">X</Button> - フォーカスインジケーター: Tailwind の
ringユーティリティ:focus-visible:ring-2 focus-visible:ring-ring - キーボード操作: Tab キーで移動でき、Enter/Space キーでアクティブにできること
- 無効状態: 視覚的な状態と
aria-disabledまたはdisabled属性の両方を設定すること
ARIA パターン
- アイコンのみのボタン:
aria-label="Delete item" - 読み込み状態:読み込み中のコンテナに
aria-busy="true" - 動的コンテンツの更新:コンテナに
aria-live="polite" - フォームエラー:入力欄に
aria-invalid="true"、メッセージにrole="alert" - モーダル:
aria-modal="true"、フォーカストラップ、Escape キーで閉じる - ナビゲーションランドマーク:
<nav aria-label="Main navigation">
例
ユーザープロフィールカード
interface UserProfileCardProps {
user: { id: string; name: string; role: string; avatarUrl: string };
onEdit?: (userId: string) => void;
className?: string;
}
export function UserProfileCard({ user, onEdit, className }: UserProfileCardProps) {
return (
<Card className={cn("p-6", className)}>
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={user.avatarUrl} alt={user.name} />
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-1">
<h3 className="text-sm font-medium leading-none">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.role}</p>
</div>
</div>
{onEdit && (
<div className="mt-4">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(user.id)}
aria-label={`Edit ${user.name}'s profile`}
>
Edit Profile
</Button>
</div>
)}
</Card>
);
}
SaaS ダッシュボードのデザイントークン設定
Tailwind 設定の拡張:
// tailwind.config.ts
import type { Config } from "tailwindcss";
export default {
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
} satisfies Config;
レスポンシブダッシュボードレイアウト
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-background">
<nav className="border-b border-border" aria-label="Main navigation">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center">
{/* nav content */}
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-8">
<aside className="hidden lg:block" aria-label="Sidebar">
{/* sidebar content */}
</aside>
<main>{children}</main>
</div>
</div>
</div>
);
}
エッジケース
-
**既存のデザイントークンがない場合:**スターター用のトークンセット(
references/design-tokens-reference.mdを参照)を生成し、コンポーネントコードを書く前にユーザーに提示して確認を求めます。「デザイントークンが見つかりませんでした。こちらがスターター用のトークンセットです。適用しますか?」 -
**shadcn/uiがインストールされていない場合:**同等のパターンを使ったプレーンなTailwindに切り替えます。
<Button>の代わりに<button className="...">を、<Card>の代わりにスタイルを設定した<div>を使用します。同じスペーシングとカラートークンのアプローチを維持します。 -
**コンポーネントが重複する場合:**リクエストされたコンポーネントが既存のものと重複する場合は、その旨を伝えます。「類似した
UserCardコンポーネントがsrc/components/UserCard.tsxにあります。これを拡張しますか、それとも別のコンポーネントを作成しますか?」 -
ダークモードのトークンがない場合:
:rootトークンは存在するものの.darkバリアントがない場合は、作業を進める前に対応するダークバリアントを生成します。すべてのセマンティックカラートークンに、ライトとダーク両方の値が必要です。 -
**カスタムブランドカラー:**ユーザーが特定のブランドカラーの16進数値を指定した場合は、HSLに変換してトークンシステムに組み込みます。コンポーネント内で16進数値を直接使用しないでください。
-
**既存コードのスペーシングに一貫性がない場合:**一貫性がないことを伝え、最も近い8ptグリッドの値を提案し、既存のコンポーネントを正規化するか、新しいコードにのみグリッドを適用するかを確認します。
スターター用のトークンセット、カラーパレットのガイド、タイポグラフィのスケールについては、references/design-tokens-reference.mdを参照してください。