主题系统 #

一、ThemeProvider 基础 #

1.1 基本使用 #

jsx
import styled, { ThemeProvider } from 'styled-components';

const theme = {
  colors: {
    primary: '#667eea',
    secondary: '#764ba2',
    background: '#ffffff',
    text: '#333333',
  },
  fonts: {
    main: 'Inter, sans-serif',
    code: 'Fira Code, monospace',
  },
};

const Button = styled.button`
  background: ${props => props.theme.colors.primary};
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
`;

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>Themed Button</Button>
    </ThemeProvider>
  );
}

1.2 主题传递机制 #

text
主题传递流程
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  <ThemeProvider theme={theme}>                              │
│         │                                                   │
│         ▼                                                   │
│  ┌─────────────────────────────────────┐                   │
│  │  React Context                      │                   │
│  │  提供主题数据                        │                   │
│  └─────────────────────────────────────┘                   │
│         │                                                   │
│         ▼                                                   │
│  <Button>                                                   │
│    ${props => props.theme.colors.primary}                  │
│  </Button>                                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、主题定义 #

2.1 完整主题结构 #

jsx
const theme = {
  colors: {
    primary: {
      main: '#667eea',
      light: '#a5b4fc',
      dark: '#4c51bf',
    },
    secondary: {
      main: '#764ba2',
      light: '#c4b5fd',
      dark: '#6b21a8',
    },
    success: {
      main: '#22c55e',
      light: '#86efac',
      dark: '#16a34a',
    },
    warning: {
      main: '#f59e0b',
      light: '#fcd34d',
      dark: '#d97706',
    },
    danger: {
      main: '#ef4444',
      light: '#fca5a5',
      dark: '#dc2626',
    },
    neutral: {
      white: '#ffffff',
      black: '#000000',
      gray50: '#f9fafb',
      gray100: '#f3f4f6',
      gray200: '#e5e7eb',
      gray300: '#d1d5db',
      gray400: '#9ca3af',
      gray500: '#6b7280',
      gray600: '#4b5563',
      gray700: '#374151',
      gray800: '#1f2937',
      gray900: '#111827',
    },
  },
  typography: {
    fontFamily: {
      main: 'Inter, -apple-system, sans-serif',
      heading: 'Poppins, sans-serif',
      code: 'Fira Code, monospace',
    },
    fontSize: {
      xs: '0.75rem',
      sm: '0.875rem',
      base: '1rem',
      lg: '1.125rem',
      xl: '1.25rem',
      '2xl': '1.5rem',
      '3xl': '1.875rem',
      '4xl': '2.25rem',
      '5xl': '3rem',
    },
    fontWeight: {
      normal: 400,
      medium: 500,
      semibold: 600,
      bold: 700,
    },
    lineHeight: {
      tight: 1.25,
      normal: 1.5,
      relaxed: 1.75,
    },
  },
  spacing: {
    0: '0',
    1: '0.25rem',
    2: '0.5rem',
    3: '0.75rem',
    4: '1rem',
    5: '1.25rem',
    6: '1.5rem',
    8: '2rem',
    10: '2.5rem',
    12: '3rem',
    16: '4rem',
    20: '5rem',
    24: '6rem',
  },
  borderRadius: {
    none: '0',
    sm: '0.25rem',
    base: '0.5rem',
    md: '0.375rem',
    lg: '0.5rem',
    xl: '0.75rem',
    '2xl': '1rem',
    '3xl': '1.5rem',
    full: '9999px',
  },
  shadows: {
    sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
    base: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
    md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
    xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
    '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
  },
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
    '2xl': '1536px',
  },
  transitions: {
    fast: '150ms',
    normal: '300ms',
    slow: '500ms',
  },
  zIndex: {
    dropdown: 1000,
    sticky: 1020,
    fixed: 1030,
    modalBackdrop: 1040,
    modal: 1050,
    popover: 1060,
    tooltip: 1070,
  },
};

2.2 使用主题值 #

jsx
const Card = styled.div`
  padding: ${props => props.theme.spacing[6]};
  background: ${props => props.theme.colors.neutral.white};
  border-radius: ${props => props.theme.borderRadius.xl};
  box-shadow: ${props => props.theme.shadows.lg};
`;

const Title = styled.h1`
  font-family: ${props => props.theme.typography.fontFamily.heading};
  font-size: ${props => props.theme.typography.fontSize['3xl']};
  font-weight: ${props => props.theme.typography.fontWeight.bold};
  color: ${props => props.theme.colors.neutral.gray900};
  margin-bottom: ${props => props.theme.spacing[4]};
`;

const Text = styled.p`
  font-family: ${props => props.theme.typography.fontFamily.main};
  font-size: ${props => props.theme.typography.fontSize.base};
  line-height: ${props => props.theme.typography.lineHeight.relaxed};
  color: ${props => props.theme.colors.neutral.gray600};
`;

三、主题切换 #

3.1 明暗主题 #

jsx
import styled, { ThemeProvider } from 'styled-components';
import { useState } from 'react';

const lightTheme = {
  colors: {
    background: '#ffffff',
    surface: '#f9fafb',
    text: '#111827',
    textSecondary: '#6b7280',
    primary: '#667eea',
    border: '#e5e7eb',
  },
};

const darkTheme = {
  colors: {
    background: '#111827',
    surface: '#1f2937',
    text: '#f9fafb',
    textSecondary: '#9ca3af',
    primary: '#818cf8',
    border: '#374151',
  },
};

const Container = styled.div`
  min-height: 100vh;
  background: ${props => props.theme.colors.background};
  color: ${props => props.theme.colors.text};
  transition: background 0.3s, color 0.3s;
`;

const Card = styled.div`
  padding: 24px;
  background: ${props => props.theme.colors.surface};
  border: 1px solid ${props => props.theme.colors.border};
  border-radius: 12px;
`;

const ToggleButton = styled.button`
  padding: 12px 24px;
  background: ${props => props.theme.colors.primary};
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
`;

function App() {
  const [isDark, setIsDark] = useState(false);
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <Container>
        <Card>
          <h1>Theme Switcher</h1>
          <ToggleButton onClick={() => setIsDark(!isDark)}>
            Switch to {isDark ? 'Light' : 'Dark'} Mode
          </ToggleButton>
        </Card>
      </Container>
    </ThemeProvider>
  );
}

3.2 系统主题检测 #

jsx
import { useState, useEffect } from 'react';
import styled, { ThemeProvider } from 'styled-components';

function useSystemTheme() {
  const [isDark, setIsDark] = useState(false);
  
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setIsDark(mediaQuery.matches);
    
    const handler = (e) => setIsDark(e.matches);
    mediaQuery.addEventListener('change', handler);
    
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);
  
  return isDark;
}

function App() {
  const systemIsDark = useSystemTheme();
  const [userTheme, setUserTheme] = useState(null);
  
  const isDark = userTheme ?? systemIsDark;
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <Container>
        <button onClick={() => setUserTheme(!isDark)}>
          Toggle Theme
        </button>
        <button onClick={() => setUserTheme(null)}>
          Use System Theme
        </button>
      </Container>
    </ThemeProvider>
  );
}

3.3 主题持久化 #

jsx
import { useState, useEffect } from 'react';

function usePersistedTheme(key, defaultValue) {
  const [theme, setTheme] = useState(() => {
    if (typeof window === 'undefined') return defaultValue;
    
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : defaultValue;
  });
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(theme));
  }, [key, theme]);
  
  return [theme, setTheme];
}

function App() {
  const [isDark, setIsDark] = usePersistedTheme('theme', false);
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <AppContent />
    </ThemeProvider>
  );
}

四、嵌套主题 #

4.1 主题覆盖 #

jsx
const baseTheme = {
  colors: {
    primary: '#667eea',
    background: '#ffffff',
  },
};

const sectionTheme = {
  colors: {
    primary: '#764ba2',
    background: '#f5f5f5',
  },
};

function App() {
  return (
    <ThemeProvider theme={baseTheme}>
      <Section>
        <Button>Base Theme Button</Button>
      </Section>
      
      <ThemeProvider theme={sectionTheme}>
        <Section>
          <Button>Section Theme Button</Button>
        </Section>
      </ThemeProvider>
    </ThemeProvider>
  );
}

4.2 主题合并 #

jsx
import { useMemo } from 'react';

function ThemedSection({ variant, children }) {
  const theme = useTheme();
  
  const mergedTheme = useMemo(() => ({
    ...theme,
    colors: {
      ...theme.colors,
      primary: variant === 'danger' ? '#ff4d4f' : theme.colors.primary,
    },
  }), [theme, variant]);
  
  return (
    <ThemeProvider theme={mergedTheme}>
      {children}
    </ThemeProvider>
  );
}

function App() {
  return (
    <ThemeProvider theme={baseTheme}>
      <ThemedSection variant="danger">
        <Button>Danger Button</Button>
      </ThemedSection>
    </ThemeProvider>
  );
}

五、useTheme Hook #

5.1 基本使用 #

jsx
import styled, { useTheme } from 'styled-components';

const ThemedComponent = styled.div`
  padding: 24px;
  background: ${props => props.theme.colors.background};
`;

function CustomComponent() {
  const theme = useTheme();
  
  return (
    <div style={{ 
      color: theme.colors.primary,
      fontSize: theme.typography.fontSize.lg,
    }}>
      Custom styled with theme
    </div>
  );
}

5.2 条件样式 #

jsx
function ResponsiveComponent() {
  const theme = useTheme();
  const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.md})`);
  
  return (
    <div style={{
      padding: isMobile ? theme.spacing[4] : theme.spacing[8],
      fontSize: isMobile ? theme.typography.fontSize.sm : theme.typography.fontSize.base,
    }}>
      Responsive content
    </div>
  );
}

六、TypeScript 支持 #

6.1 扩展主题类型 #

typescript
import 'styled-components';
import { DefaultTheme } from 'styled-components';

declare module 'styled-components' {
  export interface DefaultTheme {
    colors: {
      primary: string;
      secondary: string;
      background: string;
      text: string;
    };
    spacing: {
      [key: number]: string;
    };
    borderRadius: {
      sm: string;
      md: string;
      lg: string;
    };
  }
}

6.2 类型安全的主题 #

tsx
import styled, { ThemeProvider, DefaultTheme } from 'styled-components';

const theme: DefaultTheme = {
  colors: {
    primary: '#667eea',
    secondary: '#764ba2',
    background: '#ffffff',
    text: '#333333',
  },
  spacing: {
    1: '0.25rem',
    2: '0.5rem',
    4: '1rem',
    8: '2rem',
  },
  borderRadius: {
    sm: '0.25rem',
    md: '0.5rem',
    lg: '1rem',
  },
};

const Button = styled.button`
  padding: ${props => props.theme.spacing[2]} ${props => props.theme.spacing[4]};
  background: ${props => props.theme.colors.primary};
  border-radius: ${props => props.theme.borderRadius.md};
`;

七、设计系统构建 #

7.1 设计 Token #

jsx
const tokens = {
  colors: {
    brand: {
      primary: '#667eea',
      secondary: '#764ba2',
    },
    semantic: {
      success: '#22c55e',
      warning: '#f59e0b',
      danger: '#ef4444',
      info: '#3b82f6',
    },
    neutral: {
      white: '#ffffff',
      black: '#000000',
      gray: {
        50: '#f9fafb',
        100: '#f3f4f6',
        200: '#e5e7eb',
        300: '#d1d5db',
        400: '#9ca3af',
        500: '#6b7280',
        600: '#4b5563',
        700: '#374151',
        800: '#1f2937',
        900: '#111827',
      },
    },
  },
  typography: {
    scale: {
      1: 0.75,
      2: 0.875,
      3: 1,
      4: 1.125,
      5: 1.25,
      6: 1.5,
      7: 1.875,
      8: 2.25,
      9: 3,
    },
    weights: {
      regular: 400,
      medium: 500,
      semibold: 600,
      bold: 700,
    },
  },
  spacing: {
    0: 0,
    1: 4,
    2: 8,
    3: 12,
    4: 16,
    5: 20,
    6: 24,
    8: 32,
    10: 40,
    12: 48,
    16: 64,
  },
  radii: {
    none: 0,
    sm: 4,
    md: 8,
    lg: 12,
    xl: 16,
    full: 9999,
  },
  shadows: {
    sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
    md: '0 4px 6px rgba(0, 0, 0, 0.1)',
    lg: '0 10px 15px rgba(0, 0, 0, 0.1)',
    xl: '0 20px 25px rgba(0, 0, 0, 0.15)',
  },
};

const px = (value) => `${value}px`;

const theme = {
  colors: tokens.colors,
  typography: {
    ...tokens.typography,
    fontSize: Object.fromEntries(
      Object.entries(tokens.typography.scale).map(([key, value]) => [
        key, `${value}rem`
      ])
    ),
  },
  spacing: Object.fromEntries(
    Object.entries(tokens.spacing).map(([key, value]) => [
      key, px(value)
    ])
  ),
  radii: Object.fromEntries(
    Object.entries(tokens.radii).map(([key, value]) => [
      key, px(value)
    ])
  ),
  shadows: tokens.shadows,
};

7.2 组件变体 #

jsx
const Button = styled.button`
  padding: ${props => props.theme.spacing[2]} ${props => props.theme.spacing[4]};
  font-size: ${props => props.theme.typography.fontSize[3]};
  font-weight: ${props => props.theme.typography.weights.semibold};
  border-radius: ${props => props.theme.radii.md};
  cursor: pointer;
  transition: all 0.2s;
  
  ${props => props.$variant === 'primary' && `
    background: ${props.theme.colors.brand.primary};
    color: white;
    border: none;
    
    &:hover {
      background: #5a6fd6;
    }
  `}
  
  ${props => props.$variant === 'secondary' && `
    background: transparent;
    color: ${props.theme.colors.brand.primary};
    border: 2px solid ${props.theme.colors.brand.primary};
    
    &:hover {
      background: ${props.theme.colors.brand.primary};
      color: white;
    }
  `}
  
  ${props => props.$variant === 'danger' && `
    background: ${props.theme.colors.semantic.danger};
    color: white;
    border: none;
    
    &:hover {
      background: #dc2626;
    }
  `}
`;

八、响应式主题 #

8.1 断点工具 #

jsx
const theme = {
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
  },
};

const media = Object.keys(theme.breakpoints).reduce((acc, label) => {
  acc[label] = (...args) => css`
    @media (min-width: ${theme.breakpoints[label]}) {
      ${css(...args)}
    }
  `;
  return acc;
}, {});

const ResponsiveBox = styled.div`
  padding: ${props => props.theme.spacing[4]};
  
  ${media.md`
    padding: ${props => props.theme.spacing[8]};
  `}
  
  ${media.lg`
    padding: ${props => props.theme.spacing[12]};
  `}
`;

8.2 响应式值 #

jsx
const getResponsiveValue = (props, values) => {
  const { theme } = props;
  const breakpoints = Object.keys(theme.breakpoints);
  
  return breakpoints.map((bp, index) => {
    if (values[bp]) {
      return css`
        @media (min-width: ${theme.breakpoints[bp]}) {
          ${values[bp]}
        }
      `;
    }
    return null;
  });
};

const ResponsiveText = styled.p`
  font-size: 16px;
  
  ${props => getResponsiveValue(props, {
    md: `font-size: 18px;`,
    lg: `font-size: 20px;`,
  })}
`;

九、总结 #

主题系统要点速查表:

概念 说明
ThemeProvider 提供主题上下文
props.theme 访问主题值
useTheme Hook 获取主题
嵌套主题 支持主题覆盖
TypeScript 扩展 DefaultTheme

下一步:学习 全局样式 掌握全局样式管理技巧。

最后更新:2026-03-28