AgentSkillsCN

design-system

设计令牌系统、组件架构模式,以及 UI 实施标准。适用于构建设计系统、创建可复用组件、实现暗黑模式,或在各类应用中建立视觉一致性时使用。

SKILL.md
--- frontmatter
name: design-system
description: Design token systems, component architecture patterns, and UI implementation standards. Use when building design systems, creating reusable components, implementing dark mode, or establishing visual consistency across applications.

Design System

Comprehensive guide for building scalable design systems with design tokens, component patterns, and implementation standards.

When to Use This Skill

  • Establishing design token systems (spacing, color, typography)
  • Building reusable UI component libraries
  • Implementing dark mode / theming
  • Creating consistent component APIs
  • Setting up Storybook documentation

1. Design Token System

Token Hierarchy

code
Global Tokens (primitive)
└── Semantic Tokens (purpose)
    └── Component Tokens (specific)

Example:
--color-blue-500          (global)
└── --color-primary       (semantic)
    └── --button-bg       (component)

Spacing Scale

Base unit (4px) × multiplier. Limit choices for consistency.

css
:root {
  --spacing-0: 0;
  --spacing-1: 4px;    /* Inline icon gaps */
  --spacing-2: 8px;    /* Tight padding, related elements */
  --spacing-3: 12px;   /* Default padding */
  --spacing-4: 16px;   /* Card padding, section gaps */
  --spacing-6: 24px;   /* Component separation */
  --spacing-8: 32px;   /* Section separation */
  --spacing-12: 48px;  /* Large section gaps */
  --spacing-16: 64px;  /* Page section separation */
}

Typography Scale

Modular scale (1.25 ratio) for harmonious sizing.

css
:root {
  /* Font Families */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'Fira Code', 'Consolas', monospace;

  /* Font Sizes */
  --text-xs: 0.75rem;    /* 12px - Captions, labels */
  --text-sm: 0.875rem;   /* 14px - Secondary text */
  --text-base: 1rem;     /* 16px - Body text */
  --text-lg: 1.25rem;    /* 20px - Lead text */
  --text-xl: 1.5rem;     /* 24px - H4 */
  --text-2xl: 2rem;      /* 32px - H3 */
  --text-3xl: 2.5rem;    /* 40px - H2 */
  --text-4xl: 3rem;      /* 48px - H1 */

  /* Line Heights */
  --leading-none: 1;
  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.75;

  /* Font Weights */
  --font-normal: 400;
  --font-medium: 500;
  --font-semibold: 600;
  --font-bold: 700;
}

Color System

css
:root {
  /* Palette (primitives - don't use directly) */
  --color-gray-50: #fafafa;
  --color-gray-100: #f5f5f5;
  --color-gray-200: #e5e5e5;
  --color-gray-300: #d4d4d4;
  --color-gray-400: #a3a3a3;
  --color-gray-500: #737373;
  --color-gray-600: #525252;
  --color-gray-700: #404040;
  --color-gray-800: #262626;
  --color-gray-900: #171717;

  --color-blue-50: #eff6ff;
  --color-blue-500: #3b82f6;
  --color-blue-600: #2563eb;
  --color-blue-700: #1d4ed8;

  --color-red-50: #fef2f2;
  --color-red-500: #ef4444;
  --color-red-600: #dc2626;

  --color-green-50: #f0fdf4;
  --color-green-500: #22c55e;
  --color-green-600: #16a34a;

  /* Semantic Tokens (use these) */
  --color-bg-primary: var(--color-gray-50);
  --color-bg-secondary: var(--color-gray-100);
  --color-bg-tertiary: var(--color-gray-200);
  --color-bg-inverse: var(--color-gray-900);

  --color-text-primary: var(--color-gray-900);
  --color-text-secondary: var(--color-gray-600);
  --color-text-tertiary: var(--color-gray-500);
  --color-text-inverse: var(--color-gray-50);
  --color-text-link: var(--color-blue-600);

  --color-border-default: var(--color-gray-200);
  --color-border-strong: var(--color-gray-300);

  --color-interactive: var(--color-blue-500);
  --color-interactive-hover: var(--color-blue-600);
  --color-interactive-active: var(--color-blue-700);

  --color-success: var(--color-green-500);
  --color-error: var(--color-red-500);
  --color-warning: #f59e0b;
}

Elevation (Shadow) Scale

css
:root {
  --shadow-none: none;
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);

  /* Elevation usage */
  --shadow-card: var(--shadow-sm);
  --shadow-dropdown: var(--shadow-md);
  --shadow-modal: var(--shadow-lg);
  --shadow-toast: var(--shadow-xl);
}

Motion Tokens

css
:root {
  /* Durations */
  --duration-instant: 50ms;
  --duration-fast: 100ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --duration-slower: 500ms;

  /* Easings */
  --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);

  /* Common transitions */
  --transition-colors: color, background-color, border-color var(--duration-fast) var(--ease-default);
  --transition-transform: transform var(--duration-normal) var(--ease-default);
  --transition-opacity: opacity var(--duration-normal) var(--ease-default);
}

/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-instant: 0ms;
    --duration-fast: 0ms;
    --duration-normal: 0ms;
    --duration-slow: 0ms;
  }
}

Border Radius

css
:root {
  --radius-none: 0;
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-full: 9999px;
}

2. Dark Mode Implementation

CSS Custom Properties Approach

css
/* Light theme (default) */
:root {
  --color-bg-primary: var(--color-gray-50);
  --color-bg-secondary: var(--color-gray-100);
  --color-text-primary: var(--color-gray-900);
  --color-text-secondary: var(--color-gray-600);
  --color-border-default: var(--color-gray-200);
}

/* Dark theme */
[data-theme="dark"] {
  --color-bg-primary: var(--color-gray-900);
  --color-bg-secondary: var(--color-gray-800);
  --color-text-primary: var(--color-gray-50);
  --color-text-secondary: var(--color-gray-400);
  --color-border-default: var(--color-gray-700);
}

/* System preference */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg-primary: var(--color-gray-900);
    --color-bg-secondary: var(--color-gray-800);
    --color-text-primary: var(--color-gray-50);
    --color-text-secondary: var(--color-gray-400);
    --color-border-default: var(--color-gray-700);
  }
}

React Theme Provider

tsx
// contexts/ThemeContext.tsx
'use client';

import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
  resolvedTheme: 'light' | 'dark';
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('system');
  const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    const stored = localStorage.getItem('theme') as Theme | null;
    if (stored) setTheme(stored);
  }, []);

  useEffect(() => {
    const root = document.documentElement;
    
    if (theme === 'system') {
      const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      root.setAttribute('data-theme', systemDark ? 'dark' : 'light');
      setResolvedTheme(systemDark ? 'dark' : 'light');
    } else {
      root.setAttribute('data-theme', theme);
      setResolvedTheme(theme);
    }
    
    localStorage.setItem('theme', theme);
  }, [theme]);

  // Listen for system changes
  useEffect(() => {
    if (theme !== 'system') return;
    
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = (e: MediaQueryListEvent) => {
      setResolvedTheme(e.matches ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
    };
    
    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [theme]);

  return (
    <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
};

Theme Toggle Component

tsx
// components/ThemeToggle.tsx
'use client';

import { useTheme } from '@/contexts/ThemeContext';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
    >
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
}

Prevent Flash (Next.js)

tsx
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                const theme = localStorage.getItem('theme') || 'system';
                const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
                const resolved = theme === 'system' ? (systemDark ? 'dark' : 'light') : theme;
                document.documentElement.setAttribute('data-theme', resolved);
              })();
            `,
          }}
        />
      </head>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

3. Component Architecture

Component Categories

CategoryPurposeExamples
PrimitivesAtomic building blocksBox, Text, Icon
FormsUser inputButton, Input, Select, Checkbox
LayoutStructureStack, Grid, Container, Divider
Data DisplayContentCard, Badge, Avatar, Table
FeedbackUser feedbackSpinner, Skeleton, Progress
OverlayLayered UIModal, Popover, Tooltip, Drawer

Component API Design

1. Variant over Boolean

tsx
// ❌ Bad
<Button primary large outline>

// ✅ Good
<Button variant="primary" size="lg" appearance="outline">

2. Consistent Prop Names

tsx
// Standard prop conventions
size: 'sm' | 'md' | 'lg'
variant: 'primary' | 'secondary' | 'ghost'
disabled: boolean
fullWidth: boolean

3. Compound Components

tsx
// Complex components with multiple parts
<Select>
  <Select.Trigger placeholder="Select option" />
  <Select.Content>
    <Select.Item value="1">Option 1</Select.Item>
    <Select.Item value="2">Option 2</Select.Item>
  </Select.Content>
</Select>

<Card>
  <Card.Header>
    <Card.Title>Title</Card.Title>
  </Card.Header>
  <Card.Body>Content</Card.Body>
  <Card.Footer>Actions</Card.Footer>
</Card>

Component States

Every interactive component must handle:

StateCSS SelectorDescription
Default-Resting state
Hover:hoverMouse over
Focus:focus-visibleKeyboard focus
Active:activeBeing pressed
Disabled:disabled, [aria-disabled]Non-interactive

Button Component Example

tsx
// components/Button/Button.tsx
import { forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import styles from './Button.module.css';

const buttonVariants = cva(styles.button, {
  variants: {
    variant: {
      primary: styles.primary,
      secondary: styles.secondary,
      ghost: styles.ghost,
      danger: styles.danger,
    },
    size: {
      sm: styles.sm,
      md: styles.md,
      lg: styles.lg,
    },
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md',
  },
});

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ 
    variant, 
    size, 
    isLoading, 
    leftIcon, 
    rightIcon, 
    disabled,
    children, 
    className,
    ...props 
  }, ref) => {
    return (
      <button
        ref={ref}
        className={buttonVariants({ variant, size, className })}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading && <Spinner size="sm" />}
        {!isLoading && leftIcon}
        {children}
        {!isLoading && rightIcon}
      </button>
    );
  }
);

Button.displayName = 'Button';
css
/* components/Button/Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2);
  border-radius: var(--radius-md);
  font-weight: var(--font-medium);
  transition: var(--transition-colors);
  cursor: pointer;
  border: none;
}

.button:focus-visible {
  outline: 2px solid var(--color-interactive);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Variants */
.primary {
  background: var(--color-interactive);
  color: white;
}
.primary:hover:not(:disabled) {
  background: var(--color-interactive-hover);
}

.secondary {
  background: var(--color-bg-secondary);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-default);
}
.secondary:hover:not(:disabled) {
  background: var(--color-bg-tertiary);
}

.ghost {
  background: transparent;
  color: var(--color-text-primary);
}
.ghost:hover:not(:disabled) {
  background: var(--color-bg-secondary);
}

/* Sizes */
.sm {
  height: 32px;
  padding: 0 var(--spacing-3);
  font-size: var(--text-sm);
}
.md {
  height: 40px;
  padding: 0 var(--spacing-4);
  font-size: var(--text-base);
}
.lg {
  height: 48px;
  padding: 0 var(--spacing-6);
  font-size: var(--text-lg);
}

4. Accessibility Requirements

Focus Management

css
/* Visible focus for keyboard users */
:focus-visible {
  outline: 2px solid var(--color-interactive);
  outline-offset: 2px;
}

/* Remove focus for mouse users */
:focus:not(:focus-visible) {
  outline: none;
}

Color Contrast

ElementMinimum Ratio
Normal text4.5:1
Large text (18px+ bold, 24px+)3:1
UI components3:1

Touch Targets

css
/* Minimum 44x44px touch target */
.interactive {
  min-width: 44px;
  min-height: 44px;
}

ARIA Patterns

tsx
// Button with loading state
<button aria-busy={isLoading} aria-disabled={isLoading}>
  {isLoading ? 'Loading...' : 'Submit'}
</button>

// Icon button needs label
<button aria-label="Close dialog">
  <CloseIcon aria-hidden="true" />
</button>

// Form error
<input aria-invalid={hasError} aria-describedby="error-message" />
<span id="error-message" role="alert">{errorMessage}</span>

5. File Structure

code
design-system/
├── tokens/
│   ├── colors.css
│   ├── spacing.css
│   ├── typography.css
│   ├── shadows.css
│   ├── motion.css
│   └── index.css          # Imports all tokens
├── components/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.module.css
│   │   ├── Button.stories.tsx
│   │   ├── Button.test.tsx
│   │   └── index.ts
│   ├── Input/
│   ├── Card/
│   └── index.ts           # Export all components
├── hooks/
│   └── useTheme.ts
├── contexts/
│   └── ThemeContext.tsx
└── index.ts               # Main entry point

6. Storybook Documentation

tsx
// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

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

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

export const Default: Story = {
  args: {
    children: 'Button',
  },
};

export const Variants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="danger">Danger</Button>
    </div>
  ),
};

export const Sizes: 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 States: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '1rem' }}>
      <Button>Default</Button>
      <Button disabled>Disabled</Button>
      <Button isLoading>Loading</Button>
    </div>
  ),
};

Related Skills

  • For React component patterns, see: react-patterns
  • For performance optimization, see: performance-patterns