Design System Architect について
再利用可能で一貫性のある UI 基盤を構築するチーム向けのデザインシステムスキルです。トークン、コンポーネント設計、ドキュメント、テーマ、ガバナンス、テストを扱います。
主な機能
- 01デザイントークンの構成
- 02再利用可能なコンポーネント設計
- 03Storybookによるコンポーネント文書化
- 04テーマとブランド差分の設定
- 05バージョン管理とガバナンス
- 06コンポーネントと視覚変更のテスト
活用シーン
- 01共有コンポーネントライブラリの構築
- 02ライト・ダークテーマへの対応
- 03プロダクトチーム向けUI文書の作成
- 04デザインシステムの変更とリリース管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill design-system-architectインストール後、Claude Code で/design-system-architectを入力して読み込まれたか確認します。
同梱ファイルmanifest.yamlREADME.md
詳細
デザインシステムアーキテクト
デザインシステムは、大規模に一貫性をもたらす唯一の信頼できる情報源です。
基本原則
デザインシステムは、単なるコンポーネントライブラリではありません。デザインとエンジニアリングをつなぐ共通言語です。優れたデザインシステムには、次のような特長があります。
- プロダクト開発を加速する(再利用可能なコンポーネント)
- プロダクト間の一貫性を確保する(統一されたブランド)
- アクセシビリティを向上させる(コンポーネントに組み込み済み)
- スケーラビリティを実現する(線形ではなく、複利的な成長)
- 技術的負債を減らす(保守を一元化)
**目標:**一度構築すれば、どこでも使えます。一度保守すれば、あらゆる場所で改善できます。
フェーズ 1:デザイントークン
デザイントークンとは?
**デザイントークンは、デザインシステムを構成する最小単位の値です。**色、余白、タイポグラフィなどの最小の決定事項をデータとして保存し、すべてのプラットフォームで利用します。
トークンが重要な理由:
- 唯一の信頼できる情報源(一度変更すれば、どこでも更新されます)
- プラットフォームに依存しない(JSON → CSS、iOS、Android など)
- バージョン管理と追跡が可能(デザイン上の決定を Git の履歴で確認できます)
- テーマに対応(ライト/ダークモード、ブランドのバリエーション)
トークンの構造
// tokens/colors.json
{
"color": {
"brand": {
"primary": {
"50": { "value": "#eff6ff" },
"100": { "value": "#dbeafe" },
"200": { "value": "#bfdbfe" },
"300": { "value": "#93c5fd" },
"400": { "value": "#60a5fa" },
"500": { "value": "#3b82f6" },
"600": { "value": "#2563eb" },
"700": { "value": "#1d4ed8" },
"800": { "value": "#1e40af" },
"900": { "value": "#1e3a8a" },
"950": { "value": "#172554" }
}
},
"semantic": {
"background": {
"primary": { "value": "{color.neutral.50}" },
"secondary": { "value": "{color.neutral.100}" }
},
"text": {
"primary": { "value": "{color.neutral.900}" },
"secondary": { "value": "{color.neutral.600}" }
},
"feedback": {
"success": { "value": "#10b981" },
"warning": { "value": "#f59e0b" },
"error": { "value": "#ef4444" },
"info": { "value": "#3b82f6" }
}
}
}
}
トークンのカテゴリー
プリミティブトークン(生の値):
{
"color-blue-500": "#3b82f6",
"space-4": "16px",
"font-size-base": "16px"
}
セマンティックトークン(用途に基づく名前):
{
"color-primary": "{color-blue-500}",
"spacing-default": "{space-4}",
"text-body": "{font-size-base}"
}
コンポーネントトークン(コンポーネント固有):
{
"button-padding-x": "{spacing-default}",
"button-background": "{color-primary}",
"button-text": "{color-white}"
}
トークンのアーキテクチャ
tokens/
├── primitives/
│ ├── colors.json # Raw color values
│ ├── spacing.json # 4px, 8px, 16px...
│ ├── typography.json # Font sizes, weights
│ ├── shadows.json # Elevation system
│ └── radii.json # Border radius values
├── semantic/
│ ├── colors.json # background-primary, text-secondary
│ ├── spacing.json # spacing-tight, spacing-comfortable
│ └── typography.json # heading-xl, body-md
└── components/
├── button.json # Button-specific tokens
├── input.json # Input-specific tokens
└── card.json # Card-specific tokens
Style Dictionary を使ったトークンの変換
Style Dictionary をインストールします:
npm install --save-dev style-dictionary
config.json:
{
"source": ["tokens/**/*.json"],
"platforms": {
"css": {
"transformGroup": "css",
"buildPath": "dist/css/",
"files": [
{
"destination": "variables.css",
"format": "css/variables"
}
]
},
"js": {
"transformGroup": "js",
"buildPath": "dist/js/",
"files": [
{
"destination": "tokens.js",
"format": "javascript/es6"
}
]
}
}
}
トークンをビルドします:
npx style-dictionary build
出力(variables.css):
:root {
--color-brand-primary-500: #3b82f6;
--color-semantic-background-primary: #fafafa;
--space-4: 16px;
--font-size-base: 16px;
--button-padding-x: 16px;
}
トークンを使ったダークモード
// tokens/semantic/colors-light.json
{
"background": {
"primary": { "value": "#ffffff" },
"secondary": { "value": "#f9fafb" }
},
"text": {
"primary": { "value": "#111827" },
"secondary": { "value": "#6b7280" }
}
}
// tokens/semantic/colors-dark.json
{
"background": {
"primary": { "value": "#111827" },
"secondary": { "value": "#1f2937" }
},
"text": {
"primary": { "value": "#f9fafb" },
"secondary": { "value": "#d1d5db" }
}
}
CSS の出力:
:root {
--background-primary: #ffffff;
--text-primary: #111827;
}
[data-theme='dark'] {
--background-primary: #111827;
--text-primary: #f9fafb;
}
フェーズ 2:コンポーネントアーキテクチャ
アトミックデザインの手法
アトム → モレキュール → オーガニズム → テンプレート → ページ
atoms/
├── Button/
├── Input/
├── Label/
├── Icon/
└── Text/
molecules/
├── FormField/ # Label + Input + Error
├── SearchBar/ # Input + Icon + Button
└── Card/ # Container + Text + Button
organisms/
├── Header/ # Logo + Nav + SearchBar
├── LoginForm/ # FormFields + Button
└── ProductCard/ # Card + Image + Price + CTA
templates/
├── DashboardLayout/ # Header + Sidebar + Content
└── MarketingLayout/ # Header + Hero + Features + Footer
pages/
├── HomePage/ # MarketingLayout + specific content
└── DashboardPage/ # DashboardLayout + widgets
コンポーネントの構造
components/
└── Button/
├── Button.tsx # Component implementation
├── Button.module.css # Scoped styles
├── Button.stories.tsx # Storybook stories
├── Button.test.tsx # Unit tests
├── Button.types.ts # TypeScript types
├── index.ts # Public exports
└── README.md # Component docs
Button コンポーネント(アトムの例)
Button.types.ts:
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
isLoading?: boolean
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
fullWidth?: boolean
children: React.ReactNode
}
Button.tsx:
import React from 'react'
import styles from './Button.module.css'
import { ButtonProps } from './Button.types'
export function Button({
variant = 'primary',
size = 'md',
isLoading = false,
leftIcon,
rightIcon,
fullWidth = false,
disabled,
children,
className,
...props
}: ButtonProps) {
const classes = [
styles.button,
styles[variant],
styles[size],
fullWidth && styles.fullWidth,
isLoading && styles.loading,
className
].filter(Boolean).join(' ')
return (
<button
className={classes}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{leftIcon && <span className={styles.iconLeft}>{leftIcon}</span>}
<span className={styles.content}>{children}</span>
{rightIcon && <span className={styles.iconRight}>{rightIcon}</span>}
{isLoading && (
<span className={styles.spinner} aria-label="Loading">
<svg className={styles.spinnerIcon} viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" strokeWidth="4" fill="none" strokeLinecap="round" />
</svg>
</span>
)}
</button>
)
}
Button.module.css:
.button {
/* Base */
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-base);
font-weight: 500;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 150ms ease;
user-select: none;
/* Accessibility */
outline-offset: 2px;
}
.button:focus-visible {
outline: 2px solid var(--color-focus);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Variants */
.primary {
background: var(--button-primary-background);
color: var(--button-primary-text);
}
.primary:hover:not(:disabled) {
background: var(--button-primary-background-hover);
}
.secondary {
background: transparent;
color: var(--button-secondary-text);
border: 1px solid var(--button-secondary-border);
}
.secondary:hover:not(:disabled) {
background: var(--button-secondary-background-hover);
}
.tertiary {
background: transparent;
color: var(--button-tertiary-text);
}
.tertiary:hover:not(:disabled) {
background: var(--button-tertiary-background-hover);
}
.danger {
background: var(--color-error);
color: var(--color-white);
}
.danger:hover:not(:disabled) {
background: var(--color-error-dark);
}
/* Sizes */
.sm {
padding: var(--space-1) var(--space-3);
font-size: var(--text-sm);
height: 32px;
}
.md {
padding: var(--space-2) var(--space-4);
font-size: var(--text-base);
height: 40px;
}
.lg {
padding: var(--space-3) var(--space-6);
font-size: var(--text-lg);
height: 48px;
}
/* Modifiers */
.fullWidth {
width: 100%;
}
.loading {
color: transparent;
}
.spinner {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
inset: 0;
}
.spinnerIcon {
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.iconLeft,
.iconRight {
display: flex;
align-items: center;
}
.content {
display: flex;
align-items: center;
}
FormField コンポーネント(モレキュールの例)
FormField.tsx:
import React from 'react'
import styles from './FormField.module.css'
interface FormFieldProps {
label: string
error?: string
hint?: string
required?: boolean
children: React.ReactNode
}
export function FormField({
label,
error,
hint,
required = false,
children
}: FormFieldProps) {
const inputId = React.useId()
const errorId = `${inputId}-error`
const hintId = `${inputId}-hint`
// Clone child to pass accessibility props
const childWithProps = React.cloneElement(
children as React.ReactElement,
{
id: inputId,
'aria-invalid': !!error,
'aria-describedby': [
hint ? hintId : null,
error ? errorId : null
].filter(Boolean).join(' ') || undefined
}
)
return (
<div className={styles.field}>
<label htmlFor={inputId} className={styles.label}>
{label}
{required && (
<span className={styles.required} aria-label="required">
*
</span>
)}
</label>
{hint && (
<div id={hintId} className={styles.hint}>
{hint}
</div>
)}
{childWithProps}
{error && (
<div id={errorId} className={styles.error} role="alert">
{error}
</div>
)}
</div>
)
}
コンポーネント API 設計の原則
1. 適切なデフォルト値:
// ✅ Good: Works with minimal props
<Button>Click me</Button>
// ✅ Good: Customizable when needed
<Button variant="secondary" size="lg" fullWidth>
Click me
</Button>
2. 設定よりもコンポジションを優先:
// ❌ Bad: Too many props
<Modal
title="Delete Account"
body="Are you sure?"
confirmText="Delete"
cancelText="Cancel"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
// ✅ Good: Composable
<Modal>
<Modal.Header>Delete Account</Modal.Header>
<Modal.Body>Are you sure?</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={handleDelete}>Delete</Button>
<Button variant="secondary" onClick={handleCancel}>Cancel</Button>
</Modal.Footer>
</Modal>
3. 制御モードと非制御モード:
// Uncontrolled (internal state)
<Input defaultValue="hello" />
// Controlled (external state)
<Input value={value} onChange={setValue} />
4. 多相コンポーネント:
interface ButtonProps<T extends React.ElementType = 'button'> {
as?: T
// ... other props
}
// Render as button (default)
<Button onClick={handleClick}>Click</Button>
// Render as link
<Button as="a" href="/dashboard">Dashboard</Button>
// Render as Next.js Link
<Button as={Link} href="/about">About</Button>
フェーズ 3:Storybook のドキュメント
Storybook のセットアップ
Storybook をインストールします:
npx storybook@latest init
Button.stories.tsx:
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'tertiary', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
isLoading: { control: 'boolean' },
fullWidth: { control: 'boolean' },
disabled: { control: 'boolean' },
},
}
export default meta
type Story = StoryObj<typeof Button>
// Primary story
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
}
// All variants
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="danger">Danger</Button>
</div>
),
}
// All sizes
export const AllSizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
),
}
// With icons
export const WithIcons: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem' }}>
<Button leftIcon={<Icon name="plus" />}>Add Item</Button>
<Button rightIcon={<Icon name="arrow-right" />}>Next</Button>
</div>
),
}
// Loading state
export const Loading: Story = {
args: {
isLoading: true,
children: 'Loading...',
},
}
// Disabled state
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled',
},
}
// Full width
export const FullWidth: Story = {
args: {
fullWidth: true,
children: 'Full Width Button',
},
}
MDX ドキュメント
Button.mdx:
import { Meta, Story, Canvas, Controls } from '@storybook/blocks'
import * as ButtonStories from './Button.stories'
<Meta of={ButtonStories} />
# Button
Buttons allow users to trigger actions and make choices with a single tap.
## When to Use
- Primary actions (submit forms, complete workflows)
- Secondary actions (cancel, go back)
- Tertiary actions (view details, learn more)
- Dangerous actions (delete, remove)
## Variants
<Canvas of={ButtonStories.AllVariants} />
### Primary
Use for the main action on a page. Limit to one per screen.
### Secondary
Use for less important actions. Can have multiple per screen.
### Tertiary
Use for the least important actions, like "Learn more" links.
### Danger
Use for destructive actions like deleting data.
## Sizes
<Canvas of={ButtonStories.AllSizes} />
- **Small**: Dense UIs, table actions
- **Medium**: Default for most use cases
- **Large**: Hero CTAs, mobile-first designs
## With Icons
<Canvas of={ButtonStories.WithIcons} />
Icons can clarify the button's purpose or indicate direction.
## States
### Loading
<Canvas of={ButtonStories.Loading} />
Show a loading spinner when an async action is in progress.
### Disabled
<Canvas of={ButtonStories.Disabled} />
Disable buttons when an action is not currently available.
## Accessibility
- ✅ Keyboard accessible (Tab, Enter/Space)
- ✅ Focus visible indicator
- ✅ ARIA attributes (aria-busy, aria-disabled)
- ✅ Loading state announced to screen readers
## Props
<Controls />
## Usage
```tsx
import { Button } from '@/components/Button'
function MyComponent() {
return (
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
)
}
```
### Storybook Addons
```bash
# Accessibility testing
npm install --save-dev @storybook/addon-a11y
# Component interactions
npm install --save-dev @storybook/addon-interactions
# Design tokens addon
npm install --save-dev storybook-addon-designs
.storybook/main.ts:
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'storybook-addon-designs'
],
framework: '@storybook/react-vite'
}
export default config
フェーズ 4:テーマ設定とカスタマイズ
CSS 変数を使う方法
tokens.css:
:root {
/* Primitives */
--color-blue-500: #3b82f6;
--color-red-500: #ef4444;
--space-4: 16px;
/* Semantic (can be overridden) */
--color-primary: var(--color-blue-500);
--color-danger: var(--color-red-500);
--spacing-default: var(--space-4);
/* Component tokens */
--button-primary-background: var(--color-primary);
--button-primary-text: white;
--button-padding: var(--spacing-default);
}
/* Theme override */
[data-theme='brand-red'] {
--color-primary: #dc2626;
}
[data-theme='dark'] {
--color-primary: #60a5fa;
--button-primary-background: var(--color-primary);
}
テーマプロバイダー(React)
ThemeProvider.tsx:
import React, { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'light' | 'dark' | 'auto'
interface ThemeContextValue {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: 'light' | 'dark'
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('auto')
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
// Load saved theme
const saved = localStorage.getItem('theme') as Theme
if (saved) setTheme(saved)
}, [])
useEffect(() => {
// Save theme
localStorage.setItem('theme', theme)
// Resolve theme
if (theme === 'auto') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setResolvedTheme(isDark ? 'dark' : 'light')
} else {
setResolvedTheme(theme)
}
}, [theme])
useEffect(() => {
// Apply theme to DOM
document.documentElement.setAttribute('data-theme', resolvedTheme)
}, [resolvedTheme])
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
複数ブランドのテーマ設定
// themes/acme.ts
export const acmeTheme = {
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6'
},
typography: {
fontFamily: 'Inter, sans-serif'
},
spacing: {
unit: 8
}
}
// themes/contoso.ts
export const contosoTheme = {
colors: {
primary: '#dc2626',
secondary: '#f59e0b'
},
typography: {
fontFamily: 'Roboto, sans-serif'
},
spacing: {
unit: 4
}
}
テーマの適用:
function applyTheme(theme: Theme) {
Object.entries(theme.colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--color-${key}`, value)
})
document.documentElement.style.setProperty('--font-base', theme.typography.fontFamily)
document.documentElement.style.setProperty('--space-unit', `${theme.spacing.unit}px`)
}
フェーズ 5:バージョン管理とガバナンス
セマンティックバージョニング
MAJOR.MINOR.PATCH
-
**MAJOR:**互換性のない変更(v1.0.0 → v2.0.0)
- props の削除
- prop の型の変更
- デフォルトの動作の変更
-
**MINOR:**新機能、後方互換性あり(v1.0.0 → v1.1.0)
- 新しい props
- 新しいコンポーネント
- 新しいバリエーション
-
**PATCH:**バグ修正(v1.0.0 → v1.0.1)
- CSS の修正
- アクセシビリティの改善
- TypeScript の修正
変更履歴
CHANGELOG.md:
# Changelog
## [2.0.0] - 2024-01-15
### Breaking Changes
- **Button:** Renamed `type` prop to `variant`
- **Input:** Removed `error` prop (use FormField wrapper instead)
### Migration Guide
```typescript
// Before
<Button type="primary">Click</Button>
// After
<Button variant="primary">Click</Button>
```
[1.5.0] - 2024-01-10
追加
- **Button:**新しい
isLoadingprop - **Input:**新しい
leftIconとrightIconprops - **Card:**新しいコンポーネント
修正
- **Button:**すべてのブラウザーでフォーカスのアウトラインが表示されるようになりました
- **Input:**プレースホルダーの色が WCAG のコントラスト要件を満たすようになりました
[1.4.1] - 2024-01-05
修正
- **Button:**ローディングスピナーが正しく中央に配置されるようになりました
- **Modal:**背景の z-index の問題を修正しました
### Component Lifecycle
**P0 (Must Have):**
- Button, Input, Label, Text, Icon
- FormField, Card, Modal
**P1 (Should Have):**
- Select, Checkbox, Radio, Switch, Textarea
- Tabs, Accordion, Dropdown, Tooltip
**P2 (Nice to Have):**
- DatePicker, Combobox, Slider, Toggle
- Toast, Drawer, Popover
**P3 (Future):**
- DataTable, Calendar, FileUpload
- Charts, Timeline, Stepper
### Deprecation Strategy
**1. Announce deprecation:**
```typescript
/**
* @deprecated Use `variant` prop instead. Will be removed in v3.0.0.
*/
export interface ButtonProps {
type?: 'primary' | 'secondary' // Deprecated
variant?: 'primary' | 'secondary' // New
}
2. 警告を表示して両方をサポートします:
export function Button({ type, variant, ...props }: ButtonProps) {
if (type) {
console.warn('Button: `type` prop is deprecated. Use `variant` instead.')
}
const finalVariant = variant || type || 'primary'
// ...
}
3. 次のメジャーバージョンで削除します:
// v3.0.0 - type prop removed entirely
export interface ButtonProps {
variant?: 'primary' | 'secondary'
}
デザインシステムのガバナンス
デザインシステムチーム:
- オーナー: 全体的なビジョンとロードマップ
- デザイナー: ビジュアルデザイン、UX パターン
- エンジニア: 実装、ツール
- コントリビューター: コンポーネントを構築するプロダクトチーム
コントリビューションの流れ:
1. Proposal → GitHub issue
2. Design Review → Figma mockup
3. API Design → TypeScript interface
4. Implementation → PR with tests + stories
5. Documentation → README + Storybook
6. Release → Semantic versioning + changelog
提案テンプレート:
## Component Name
**Problem:** What user need does this solve?
**Usage:** When should this be used?
**API Proposal:**
```typescript
interface ComponentProps {
// ...
}
```
デザインモックアップ: [Figma へのリンク]
アクセシビリティ: どのようにアクセシブルにしますか?
検討した代替案: ほかに何を検討しましたか?
---
## Testing Design Systems
### Visual Regression Testing
**Chromatic (Storybook integration):**
```bash
npm install --save-dev chromatic
# Run visual tests
npx chromatic --project-token=YOUR_TOKEN
package.json:
{
"scripts": {
"chromatic": "chromatic --exit-zero-on-changes"
}
}
コンポーネントのテスト
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument()
})
it('calls onClick when clicked', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('disables button when loading', () => {
render(<Button isLoading>Click me</Button>)
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByLabelText('Loading')).toBeInTheDocument()
})
it('applies variant classes', () => {
render(<Button variant="danger">Delete</Button>)
expect(screen.getByRole('button')).toHaveClass('danger')
})
})
アクセシビリティのテスト
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
公開と配布
NPM パッケージの設定
package.json:
{
"name": "@company/design-system",
"version": "1.0.0",
"description": "Company design system",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist", "README.md"],
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"publishConfig": {
"access": "public"
}
}
rollup.config.js:
import typescript from '@rollup/plugin-typescript'
import postcss from 'rollup-plugin-postcss'
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
}
],
plugins: [
typescript({ tsconfig: './tsconfig.json' }),
postcss({
modules: true,
extract: 'styles.css'
})
],
external: ['react', 'react-dom']
}
公開:
npm login
npm version patch # or minor, major
npm publish
利用方法
npm install @company/design-system
import { Button, Input, Card } from '@company/design-system'
import '@company/design-system/dist/styles.css'
function App() {
return (
<Card>
<Input placeholder="Email" />
<Button variant="primary">Submit</Button>
</Card>
)
}
デザインシステムのチェックリスト
基盤
- デザイントークン(色、スペーシング、タイポグラフィ、シャドウ、角丸)
- トークン変換パイプライン(Style Dictionary)
- ダークモードへの対応
- ドキュメントサイト(Storybook)
コンポーネント
- アトミックデザイン構造(アトム → オーガニズム)
- コンポーネント API のガイドライン(適切なデフォルト、コンポジション)
- すべてのコンポーネントの TypeScript 型
- CSS モジュールまたは styled-components
- アクセシビリティを組み込み済み(WCAG 2.1 AA)
ドキュメント
- すべてのコンポーネントの Storybook ストーリー
- 使用例を含む MDX ドキュメント
- Props テーブル(自動生成)
- デザインガイドライン(使用する場合、使用しない場合)
- コード例
テスト
- ユニットテスト(カバレッジ 70% 以上)
- アクセシビリティテスト(jest-axe)
- ビジュアルリグレッションテスト(Chromatic)
- 手動テストのチェックリスト
ガバナンス
- セマンティックバージョニング
- 変更履歴の管理
- コントリビューションガイドライン
- コンポーネントのライフサイクル(P0 → P3)
- 非推奨化の方針
配布
- NPM パッケージの公開
- Storybook のデプロイ
- インストールガイド
- 破壊的変更に伴う移行ガイド
ツールとリソース
トークン管理:
- Style Dictionary(トークン変換)
- Figma Tokens(Figma → コードの同期)
コンポーネント開発:
- Storybook(ドキュメント)
- Chromatic(ビジュアルテスト)
- React + TypeScript
テスト:
- Jest + React Testing Library
- jest-axe(アクセシビリティ)
- Playwright(E2E)
ビルドと配布:
- Rollup または tsup(バンドル)
- NPM(配布)
- Changesets(バージョン管理)
参考例:
- Radix UI(アクセシブルなプリミティブ)
- Chakra UI(テーマ設定可能なコンポーネント)
- Material UI(包括的なシステム)
- Ant Design(エンタープライズ向け)
関連スキル
visual-designer- デザインの基礎(色、タイポグラフィ、スペーシング)accessibility-engineer- WCAG 準拠frontend-builder- React コンポーネントのパターンtesting-strategist- コンポーネントテストの戦略
デザインシステムに完成はありません。プロダクトとともに進化します。 🎨