前端设计系统搭建:从设计 Token 到组件库
设计系统(Design System)不是大公司的专利。即使是一个小团队,有一套基本的设计系统也能显著提升开发效率和设计一致性。
设计系统的层次
设计系统
├── 设计 Token(颜色、字体、间距、圆角)
├── 基础组件(Button、Input、Card)
├── 业务组件(SearchBar、PostCard、UserMenu)
└── 模式和规范(布局模式、交互模式、文案规范)
设计 Token
Token 是设计系统的原子单位。用 CSS 变量定义:
:root {
/* 颜色 */
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-secondary: #6b7280;
--color-border: #e5e7eb;
/* 字体 */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
/* 间距 */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
/* 圆角 */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-full: 9999px;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
暗色模式只需要切换变量值:
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1a1a1a;
--color-text: #f5f5f5;
--color-text-secondary: #9ca3af;
--color-border: #374151;
}
}
基础组件
Button
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
const variants = {
primary: 'bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)]',
secondary: 'bg-transparent border border-[var(--color-border)] hover:bg-gray-50',
ghost: 'bg-transparent hover:bg-gray-100',
danger: 'bg-[var(--color-danger)] text-white hover:bg-red-600',
};
const sizes = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
function Button({ variant = 'primary', size = 'md', className = '', children, ...props }: ButtonProps) {
return (
<button
className={`inline-flex items-center justify-center rounded-[var(--radius-md)] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] disabled:opacity-50 ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
>
{children}
</button>
);
}
Input
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
function Input({ label, error, id, ...props }: InputProps) {
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<div>
{label && (
<label htmlFor={inputId} className="mb-1 block text-sm text-[var(--color-text-secondary)]">
{label}
</label>
)}
<input
id={inputId}
className={`w-full rounded-[var(--radius-md)] border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] ${
error ? 'border-[var(--color-danger)]' : 'border-[var(--color-border)]'
}`}
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? `${inputId}-error` : undefined}
{...props}
/>
{error && (
<p id={`${inputId}-error`} className="mt-1 text-xs text-[var(--color-danger)]" role="alert">
{error}
</p>
)}
</div>
);
}
业务组件
基础组件是通用的,业务组件则包含具体的业务逻辑:
function PostCard({ post }: { post: Post }) {
return (
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] p-[var(--spacing-4)] transition-shadow hover:[var(--shadow-md)]">
{post.coverImage && (
<img src={post.coverImage} alt={post.title} className="mb-[var(--spacing-3)] w-full rounded-[var(--radius-md)]" />
)}
<div className="flex items-center gap-[var(--spacing-2)] mb-[var(--spacing-2)]">
{post.category && (
<span className="text-xs text-[var(--color-primary)]">{post.category.name}</span>
)}
<time className="text-xs text-[var(--color-text-secondary)]">
{formatDate(post.publishedAt)}
</time>
</div>
<h3 className="mb-[var(--spacing-2)] text-lg font-semibold text-[var(--color-text)]">
{post.title}
</h3>
{post.excerpt && (
<p className="text-sm text-[var(--color-text-secondary)] line-clamp-2">
{post.excerpt}
</p>
)}
<div className="mt-[var(--spacing-3)] flex gap-[var(--spacing-2)]">
{post.tags?.map((tag) => (
<span key={tag._id} className="text-xs text-[var(--color-text-secondary)]">
#{tag.name}
</span>
))}
</div>
</article>
);
}
组件文档
用 Storybook 或者简单的文档页面来展示组件:
// 每个组件都有一个简单的展示页面
export default function ButtonShowcase() {
return (
<div className="space-y-4 p-8">
<h2>Button 组件</h2>
<div className="flex gap-4">
<Button variant="primary">主要按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="ghost">幽灵按钮</Button>
<Button variant="danger">危险按钮</Button>
</div>
<div className="flex gap-4">
<Button size="sm">小号</Button>
<Button size="md">中号</Button>
<Button size="lg">大号</Button>
</div>
<Button disabled>禁用状态</Button>
</div>
);
}
设计系统和 Tailwind
如果你用 Tailwind CSS,设计 Token 可以直接用 Tailwind 的主题配置来管理:
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
success: 'var(--color-success)',
},
},
},
};
这样 Tailwind 的类名直接引用 CSS 变量,修改 Token 值就能全局生效。
不需要一开始就做大
设计系统是一个渐进的过程:
- 先定义颜色、字体、间距等基础 Token
- 然后提取最常用的基础组件(Button、Input)
- 随着项目发展,逐步添加更多组件
- 最后形成文档和规范
不要为了搭设计系统而搭设计系统。当你的项目中有超过 3 个页面使用相似的 UI 模式时,就是提取组件的好时机。
ReactCSSTailwindCSS
返回首页