mcpField
D
Design System Architect

Design System Architect

github.com/daffy0208デザイン・UI38223 回インストール44 週間前に更新
リポジトリ

Design System Architect について

再利用可能で一貫性のある UI 基盤を構築するチーム向けのデザインシステムスキルです。トークン、コンポーネント設計、ドキュメント、テーマ、ガバナンス、テストを扱います。

主な機能

  1. 01デザイントークンの構成
  2. 02再利用可能なコンポーネント設計
  3. 03Storybookによるコンポーネント文書化
  4. 04テーマとブランド差分の設定
  5. 05バージョン管理とガバナンス
  6. 06コンポーネントと視覚変更のテスト

活用シーン

  1. 01共有コンポーネントライブラリの構築
  2. 02ライト・ダークテーマへの対応
  3. 03プロダクトチーム向けUI文書の作成
  4. 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:**新しい isLoading prop
  • **Input:**新しい leftIcon と rightIcon props
  • **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 - コンポーネントテストの戦略

デザインシステムに完成はありません。プロダクトとともに進化します。 🎨

コメント