The Agent Skills Directory
npx skills add https://smithery.ai/skills/daffy0208/design-system-architect设计系统是规模化一致性的单一事实来源。
设计系统不仅仅是组件库——它们是设计与工程之间的共享语言。一个好的设计系统:
目标: 一次构建,随处使用。一次维护,处处改进。
设计令牌是设计系统的原子值。 它们是最小的设计决策(颜色、间距、排版),以数据形式存储并被所有平台使用。
令牌的重要性:
// 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 # 原始颜色值
│ ├── spacing.json # 4px, 8px, 16px...
│ ├── typography.json # 字体大小、粗细
│ ├── shadows.json # 层级系统
│ └── radii.json # 边框半径值
├── semantic/
│ ├── colors.json # background-primary, text-secondary
│ ├── spacing.json # spacing-tight, spacing-comfortable
│ └── typography.json # heading-xl, body-md
└── components/
├── button.json # 按钮特定令牌
├── input.json # 输入框特定令牌
└── card.json # 卡片特定令牌
安装 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;
}
原子 → 分子 → 有机体 → 模板 → 页面
atoms/
├── Button/
├── Input/
├── Label/
├── Icon/
└── Text/
molecules/
├── FormField/ # 标签 + 输入框 + 错误信息
├── SearchBar/ # 输入框 + 图标 + 按钮
└── Card/ # 容器 + 文本 + 按钮
organisms/
├── Header/ # 徽标 + 导航 + 搜索栏
├── LoginForm/ # 表单字段 + 按钮
└── ProductCard/ # 卡片 + 图片 + 价格 + 行动号召
templates/
├── DashboardLayout/ # 页眉 + 侧边栏 + 内容区
└── MarketingLayout/ # 页眉 + 英雄区 + 功能 + 页脚
pages/
├── HomePage/ # 营销布局 + 特定内容
└── DashboardPage/ # 仪表板布局 + 小部件
components/
└── Button/
├── Button.tsx # 组件实现
├── Button.module.css # 作用域样式
├── Button.stories.tsx # Storybook 故事
├── Button.test.tsx # 单元测试
├── Button.types.ts # TypeScript 类型
├── index.ts # 公共导出
└── README.md # 组件文档
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 {
/* 基础 */
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;
/* 可访问性 */
outline-offset: 2px;
}
.button:focus-visible {
outline: 2px solid var(--color-focus);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* 变体 */
.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);
}
/* 尺寸 */
.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;
}
/* 修饰符 */
.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.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`
// 克隆子元素以传递可访问性属性
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>
)
}
1. 合理的默认值:
// ✅ 好:使用最少的属性即可工作
<Button>点击我</Button>
// ✅ 好:需要时可自定义
<Button variant="secondary" size="lg" fullWidth>
点击我
</Button>
2. 组合优于配置:
// ❌ 差:属性过多
<Modal
title="删除账户"
body="确定吗?"
confirmText="删除"
cancelText="取消"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
// ✅ 好:可组合
<Modal>
<Modal.Header>删除账户</Modal.Header>
<Modal.Body>确定吗?</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={handleDelete}>删除</Button>
<Button variant="secondary" onClick={handleCancel}>取消</Button>
</Modal.Footer>
</Modal>
3. 受控与非受控模式:
// 非受控(内部状态)
<Input defaultValue="hello" />
// 受控(外部状态)
<Input value={value} onChange={setValue} />
4. 多态组件:
interface ButtonProps<T extends React.ElementType = 'button'> {
as?: T
// ... 其他属性
}
// 渲染为按钮(默认)
<Button onClick={handleClick}>点击</Button>
// 渲染为链接
<Button as="a" href="/dashboard">仪表板</Button>
// 渲染为 Next.js Link
<Button as={Link} href="/about">关于</Button>
安装 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>
// 主要故事
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
}
// 所有变体
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>
),
}
// 所有尺寸
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>
),
}
// 带图标
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>
),
}
// 加载状态
export const Loading: Story = {
args: {
isLoading: true,
children: 'Loading...',
},
}
// 禁用状态
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled',
},
}
// 全宽
export const FullWidth: Story = {
args: {
fullWidth: true,
children: 'Full Width Button',
},
}
Button.mdx:
import { Meta, Story, Canvas, Controls } from '@storybook/blocks'
import * as ButtonStories from './Button.stories'
<Meta of={ButtonStories} />
# 按钮
按钮允许用户通过一次点击触发操作并做出选择。
## 何时使用
- 主要操作(提交表单,完成工作流)
- 次要操作(取消,返回)
- 三级操作(查看详情,了解更多)
- 危险操作(删除,移除)
## 变体
<Canvas of={ButtonStories.AllVariants} />
### 主要
用于页面上的主要操作。每个屏幕限制一个。
### 次要
用于不太重要的操作。每个屏幕可以有多个。
### 三级
用于最不重要的操作,例如“了解更多”链接。
### 危险
用于破坏性操作,如删除数据。
## 尺寸
<Canvas of={ButtonStories.AllSizes} />
- **小**:密集 UI,表格操作
- **中**:大多数用例的默认值
- **大**:英雄区行动号召,移动优先设计
## 带图标
<Canvas of={ButtonStories.WithIcons} />
图标可以阐明按钮的用途或指示方向。
## 状态
### 加载中
<Canvas of={ButtonStories.Loading} />
当异步操作正在进行时显示加载旋转器。
### 禁用
<Canvas of={ButtonStories.Disabled} />
当操作当前不可用时禁用按钮。
## 可访问性
- ✅ 键盘可访问(Tab, Enter/Space)
- ✅ 焦点可见指示器
- ✅ ARIA 属性(aria-busy, aria-disabled)
- ✅ 加载状态向屏幕阅读器宣布
## 属性
<Controls />
## 用法
```tsx
import { Button } from '@/components/Button'
function MyComponent() {
return (
<Button variant="primary" onClick={handleClick}>
点击我
</Button>
)
}
# 可访问性测试
npm install --save-dev @storybook/addon-a11y
# 组件交互
npm install --save-dev @storybook/addon-interactions
# 设计令牌插件
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
tokens.css:
:root {
/* 原始值 */
--color-blue-500: #3b82f6;
--color-red-500: #ef4444;
--space-4: 16px;
/* 语义(可被覆盖) */
--color-primary: var(--color-blue-500);
--color-danger: var(--color-red-500);
--spacing-default: var(--space-4);
/* 组件令牌 */
--button-primary-background: var(--color-primary);
--button-primary-text: white;
--button-padding: var(--spacing-default);
}
/* 主题覆盖 */
[data-theme='brand-red'] {
--color-primary: #dc2626;
}
[data-theme='dark'] {
--color-primary: #60a5fa;
--button-primary-background: var(--color-primary);
}
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(() => {
// 加载保存的主题
const saved = localStorage.getItem('theme') as Theme
if (saved) setTheme(saved)
}, [])
useEffect(() => {
// 保存主题
localStorage.setItem('theme', theme)
// 解析主题
if (theme === 'auto') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setResolvedTheme(isDark ? 'dark' : 'light')
} else {
setResolvedTheme(theme)
}
}, [theme])
useEffect(() => {
// 将主题应用到 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`)
}
主版本号.次版本号.修订号
CHANGELOG.md:
# 变更日志
## [2.0.0] - 2024-01-15
### 破坏性变更
- **按钮:** 将 `type` 属性重命名为 `variant`
- **输入框:** 移除了 `error` 属性(改用 FormField 包装器)
### 迁移指南
```typescript
// 之前
<Button type="primary">点击</Button>
// 之后
<Button variant="primary">点击</Button>
isLoading 属性leftIcon 和 rightIcon 属性
### 组件生命周期
**P0(必须有):**
- 按钮、输入框、标签、文本、图标
- 表单字段、卡片、模态框
**P1(应该有):**
- 选择框、复选框、单选按钮、开关、文本区域
- 标签页、手风琴、下拉菜单、工具提示
**P2(最好有):**
- 日期选择器、组合框、滑块、切换开关
- 提示框、抽屉、弹出框
**P3(未来):**
- 数据表、日历、文件上传
- 图表、时间线、步骤条
### 弃用策略
**1. 宣布弃用:**
```typescript
/**
* @deprecated 请使用 `variant` 属性。将在 v3.0.0 中移除。
*/
export interface ButtonProps {
type?: 'primary' | 'secondary' // 已弃用
variant?: 'primary' | 'secondary' // 新属性
}
2. 同时支持两者(带警告):
export function Button({ type, variant, ...props }: ButtonProps) {
if (type) {
console.warn('Button: `type` 属性已弃用。请使用 `variant`。')
}
const finalVariant = variant || type || 'primary'
// ...
}
3. 在下一个主版本中移除:
// v3.0.0 - type 属性完全移除
export interface ButtonProps {
variant?: 'primary' | 'secondary'
}
设计系统团队:
贡献流程:
1. 提案 → GitHub issue
2. 设计评审 → Figma 模型
3. API 设计 → TypeScript 接口
4. 实现 → 包含测试 + 故事的 PR
5. 文档 → README + Storybook
6. 发布 → 语义化版本控制 + 变更日志
提案模板:
## 组件名称
**问题:** 这解决了什么用户需求?
**用法:** 应该在何时使用?
**API 提案:**
```typescript
interface ComponentProps {
// ...
}
设计模型: [Figma 链接]
可访问性: 这将如何实现可访问性?
考虑的替代方案: 我们还探索了什么?
---
## 测试设计系统
### 视觉回归测试
**Chromatic(Storybook 集成):**
```bash
npm install --save-dev chromatic
# 运行视觉测试
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('渲染子元素', () => {
render(<Button>点击我</Button>)
expect(screen.getByRole('button', { name: '点击我' })).toBeInTheDocument()
})
it('点击时调用 onClick', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button onClick={handleClick}>点击我</Button>)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('加载时禁用按钮', () => {
render(<Button isLoading>点击我</Button>)
expect(screen.getByRole('button')).toBeDisabled()
expect(screen.getByLabelText('Loading')).toBeInTheDocument()
})
it('应用变体类', () => {
render(<Button variant="danger">删除</Button>)
expect(screen.getByRole('button')).toHaveClass('danger')
})
})
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('没有可访问性违规', async () => {
const { container } = render(<Button>点击我</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
package.json:
{
"name": "@company/design-system",
"version": "1.0.0",
"description": "公司设计系统",
"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 # 或 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="邮箱" />
<Button variant="primary">提交</Button>
</Card>
)
}
令牌管理:
组件开发:
测试:
构建与分发:
灵感来源:
visual-designer - 设计基础(颜色、排版、间距)accessibility-engineer - WCAG 合规性frontend-builder - React 组件模式testing-strategist - 组件测试策略设计系统永远不会完成——它随着你的产品而演进。 🎨
每周安装次数
–
来源
[smithery.ai/ski…rchitect](https://smithery.ai/skills/daffy0208/design-system-architect "smithery.ai/skills/daffy020
A design system is a single source of truth that brings consistency at scale.
Design systems are not just component libraries—they're the shared language between design and engineering. A good design system:
Goal: Build once, use everywhere. Maintain once, improve everywhere.
Design tokens are the atomic values of your design system. They're the smallest decisions (colors, spacing, typography) stored as data and consumed by all platforms.
Why Tokens Matter:
// 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" }
}
}
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
Primitive Tokens (Raw values):
{
"color-blue-500": "#3b82f6",
"space-4": "16px",
"font-size-base": "16px"
}
Semantic Tokens (Named by purpose):
{
"color-primary": "{color-blue-500}",
"spacing-default": "{space-4}",
"text-body": "{font-size-base}"
}
Component Tokens (Component-specific):
{
"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
Install 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"
}
]
}
}
}
Build tokens:
npx style-dictionary build
Output (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 Output:
:root {
--background-primary: #ffffff;
--text-primary: #111827;
}
[data-theme='dark'] {
--background-primary: #111827;
--text-primary: #f9fafb;
}
Atoms → Molecules → Organisms → Templates → Pages
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.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.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>
)
}
1. Sensible Defaults:
// ✅ Good: Works with minimal props
<Button>Click me</Button>
// ✅ Good: Customizable when needed
<Button variant="secondary" size="lg" fullWidth>
Click me
</Button>
2. Composition Over Configuration:
// ❌ 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. Controlled & Uncontrolled Modes:
// Uncontrolled (internal state)
<Input defaultValue="hello" />
// Controlled (external state)
<Input value={value} onChange={setValue} />
4. Polymorphic Components:
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>
Install 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',
},
}
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
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);
}
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
}
}
Theme Application:
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`)
}
MAJOR.MINOR.PATCH
MAJOR: Breaking changes (v1.0.0 → v2.0.0)
MINOR: New features, backwards-compatible (v1.0.0 → v1.1.0)
PATCH: Bug fixes (v1.0.0 → v1.0.1)
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>
```
isLoading propleftIcon and rightIcon propsButton: Loading spinner now centered correctly
Modal: Fixed backdrop z-index issue
P0 (Must Have):
P1 (Should Have):
P2 (Nice to Have):
P3 (Future):
1. Announce deprecation:
/**
* @deprecated Use `variant` prop instead. Will be removed in v3.0.0.
*/
export interface ButtonProps {
type?: 'primary' | 'secondary' // Deprecated
variant?: 'primary' | 'secondary' // New
}
2. Support both (with warning):
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. Remove in next major version:
// v3.0.0 - type prop removed entirely
export interface ButtonProps {
variant?: 'primary' | 'secondary'
}
Design System Team:
Contribution Flow:
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
Proposal Template:
## Component Name
**Problem:** What user need does this solve?
**Usage:** When should this be used?
**API Proposal:**
```typescript
interface ComponentProps {
// ...
}
```
Design Mockup: [Link to Figma]
Accessibility: How will this be accessible?
Alternatives Considered: What else did we explore?
---
## 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()
})
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']
}
Publish:
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>
)
}
Token Management:
Component Development:
Testing:
Build & Distribution:
Inspiration:
visual-designer - Design foundations (color, typography, spacing)accessibility-engineer - WCAG compliancefrontend-builder - React component patternstesting-strategist - Component testing strategiesA design system is never done—it evolves with your product. 🎨
Weekly Installs
–
Source
First Seen
–
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
107,800 周安装