Design System元件庫UI 設計前端工程師元件化前端教學

Design System 教學:前端工程師的元件庫建立指南

如何從零建立 Design System?完整教學含 Design Tokens、元件庫、Storybook。打造一致性的 UI 系統,提升團隊開發效率。

· 5 分鐘閱讀

同一個「按鈕」在 A 專案是綠色、在 B 專案是藍色?同一個 Modal 元件在三個地方有三種不同的實作?Design System 就是為了解決這個問題——讓整個團隊用同一套語言(Tokens)、同一套積木(元件)、同一套原則,建立一致的使用者體驗。


前言:Design System 是什麼?

很多人會把 Design System 和 Component Library(元件庫)搞混。簡單說:

  • Component Library(元件事):一堆可復用的 UI 元件(Button、Modal、Dropdown)
  • Design System(設計系統):更大的系統,包括設計語言(Tokens)、元件庫、开發原則、使用文件、甚至是設計師和工程師之間的協作流程

一個類比:

  • Component Library 是一堆磚塊
  • Design System 是蓋房子的完整方法論——包括要用什麼水泥、磚塊怎麼排列、什麼情況用什麼磚

為什麼前端工程師要關心 Design System? 因為當你的產品開始擴張、需要多個專案共享設計語言時,沒有 Design System 就會變成「到處都是看起來不太一樣的按鈕」的義大利麵。


Design System 的核心組成

1. Design Tokens(設計變數)

Design Tokens 是設計系統的原子層——所有視覺決策的變數。

// design-tokens.ts
export const tokens = {
  // 色彩
  color: {
    primary: {
      50: '#eff6ff',
      100: '#dbeafe',
      500: '#3b82f6',  // 主要品牌色
      900: '#1e3a8a',
    },
    neutral: {
      50: '#fafafa',
      500: '#737373',
      900: '#171717',
    },
    success: '#22c55e',
    danger: '#ef4444',
    warning: '#f59e0b',
  },

  // 字體
  font: {
    family: {
      sans: 'Inter, system-ui, sans-serif',
      mono: 'JetBrains Mono, monospace',
    },
    size: {
      xs: '0.75rem',    // 12px
      sm: '0.875rem',   // 14px
      base: '1rem',     // 16px
      lg: '1.125rem',   // 18px
      xl: '1.25rem',    // 20px
      '2xl': '1.5rem',  // 24px
    },
    weight: {
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700',
    },
  },

  // 間距
  spacing: {
    1: '0.25rem',  // 4px
    2: '0.5rem',   // 8px
    3: '0.75rem',  // 12px
    4: '1rem',     // 16px
    6: '1.5rem',   // 24px
    8: '2rem',     // 32px
  },

  // 圓角
  radius: {
    sm: '0.25rem',
    md: '0.375rem',
    lg: '0.5rem',
    full: '9999px',
  },
} as const;

這些 Tokens 是設計師和工程師之間的橋樑——設計師在 Figma 裡用同樣的變數命名,工程師用 CSS 變數實作,雙方永遠同步。

2. 基礎元件(Primitive Components)

基礎元件是 UI 的最小單位——通常包裝原生 HTML 元素,加上 Design Tokens 的樣式:

// src/components/primitives/Button.tsx
import React from 'react';
import { tokens } from '../tokens';

type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
  isLoading?: boolean;
}

const variantStyles: Record<ButtonVariant, React.CSSProperties> = {
  primary: { backgroundColor: tokens.color.primary[500], color: 'white' },
  secondary: { backgroundColor: tokens.color.neutral[100], color: tokens.color.neutral[900] },
  ghost: { backgroundColor: 'transparent', color: tokens.color.primary[500] },
  danger: { backgroundColor: tokens.color.danger, color: 'white' },
};

const sizeStyles: Record<ButtonSize, React.CSSProperties> = {
  sm: { padding: `${tokens.spacing[1]} ${tokens.spacing[3]}`, fontSize: tokens.font.size.sm },
  md: { padding: `${tokens.spacing[2]} ${tokens.spacing[4]}`, fontSize: tokens.font.size.base },
  lg: { padding: `${tokens.spacing[3]} ${tokens.spacing[6]}`, fontSize: tokens.font.size.lg },
};

export function Button({
  variant = 'primary',
  size = 'md',
  isLoading = false,
  children,
  disabled,
  style,
  ...props
}: ButtonProps) {
  return (
    <button
      disabled={disabled || isLoading}
      style={{
        ...variantStyles[variant],
        ...sizeStyles[size],
        borderRadius: tokens.radius.md,
        border: 'none',
        cursor: disabled || isLoading ? 'not-allowed' : 'pointer',
        opacity: disabled ? 0.5 : 1,
        fontWeight: tokens.font.weight.medium,
        transition: 'all 0.15s ease',
        ...style,
      }}
      {...props}
    >
      {isLoading ? '載入中...' : children}
    </button>
  );
}

3. 複合元件(Composite Components)

複合元件由基礎元件組合而成,封裝更複雜的 UI 邏輯:

// src/components/composite/SearchInput.tsx
import React, { useState } from 'react';
import { tokens } from '../tokens';
import { Input } from '../primitives/Input';
import { Button } from '../primitives/Button';

interface SearchInputProps {
  onSearch: (value: string) => void;
  placeholder?: string;
}

export function SearchInput({ onSearch, placeholder = '搜尋...' }: SearchInputProps) {
  const [value, setValue] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSearch(value);
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', gap: tokens.spacing[2] }}>
      <Input
        value={value}
        onChange={setValue}
        placeholder={placeholder}
        style={{ flex: 1 }}
      />
      <Button type="submit" variant="primary" size="md">
        搜尋
      </Button>
    </form>
  );
}

建立 Design System 的步驟

Step 1:盤點現有元件

在建立 Design System 之前,先把現有專案裡的元件盤點一次。問自己:

  • 有多少種不同的 Button 樣式?
  • 有多少種不同的 Colors、字體大小、間距?
  • 哪些是重複的、可以合併的?

把重複的收集起來,是 Design System 的起點。

Step 2:定義 Design Tokens

從顏色、字體、間距、圓角這四個維度開始。先從最常用的定義,不需要一開始就定義完整的系統。

Step 3:建立基礎元件

從最常用的開始:Button、Input、Card、Badge。這些是整個系統的基礎。

Step 4:建立文件

用 Storybook 建立互動式文件,讓團隊可以即時預覽每個元件的不同變體。


使用 Storybook 建立文件

Storybook 是建立元件文件的標準工具:

npm init -y
npm install -D @storybook/react
npx storybook@latest init
// src/components/primitives/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Primitives/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'ghost', 'danger'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
    isLoading: { control: 'boolean' },
    disabled: { control: 'boolean' },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    size: 'md',
    children: '主要按鈕',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    size: 'md',
    children: '次要按鈕',
  },
};

export const Loading: Story = {
  args: {
    variant: 'primary',
    isLoading: true,
    children: '載入中',
  },
};

執行 npm run storybook,就可以在瀏覽器裡看到所有元件的互動式文件。


常見問題

Q:Design System 和 Component Library 哪個先做?

A:先做 Component Library,再逐步擴展成 Design System。不要一開始就想做一個完美的完整系統——從共享元件開始,看到價值了再逐步擴展。

Q:Design System 一定要用 Figma 嗎?

A:不一定,但設計師和工程師必須用同一套 Tokens。如果設計師在 Figma,工程師必須能從 Figma 匯出 Tokens(如使用 Token Studio)。Design System 的核心價值就是消除「設計和工程不一致」的問題。

Q:只有我一個人,有必要建立 Design System 嗎?

A:如果只有一個專案,不需要刻意建立 Design System。但可以把常用的元件抽出來做成 internal package,累積到 3-5 個專案需要共享時,再正規化。Design System 的協作價值在多人團隊才明顯。

Q:要用什麼工具建立 Design System?

A:常用組合:

  • Tokens + Components:TypeScript + CSS-in-JS 或 Vanilla Extract
  • 文件:Storybook(最流行)或 Ladle(更快、更簡單)
  • 發布:npm package(internal 公司內部 package)

總結:Design System 是協作的語言

Design System 的核心價值:讓團隊用同一套語言說話,而不是各說各話

這篇文章涵蓋:

  • Design System 與 Component Library 的區別
  • Design Tokens 的概念與實作
  • 基礎元件與複合元件的建立方式
  • Storybook 文件的建立方法
  • 建立 Design System 的步驟

下一步:選擇你們團隊最常用的 3 個元件(通常是 Button、Input、Card),把它們抽出來,加上 Design Tokens,開始建立你們的第一個 internal 元件庫。


延伸閱讀