主题系统 #
一、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