主题定制 #

基本概念 #

Emotion 提供了强大的主题系统,通过 ThemeProvider 组件可以在应用中共享主题数据,实现统一的样式管理和主题切换功能。

基本用法 #

定义主题 #

创建主题对象:

jsx
const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    success: '#28a745',
    danger: '#dc3545',
    warning: '#ffc107',
    info: '#17a2b8',
    light: '#f8f9fa',
    dark: '#343a40',
    white: '#ffffff',
    black: '#000000',
    text: '#333333',
    textMuted: '#6c757d',
    background: '#ffffff',
  },
  fontSizes: {
    xs: '12px',
    sm: '14px',
    md: '16px',
    lg: '18px',
    xl: '20px',
    xxl: '24px',
    xxxl: '32px',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
    xxl: '48px',
  },
  borderRadius: {
    sm: '4px',
    md: '8px',
    lg: '12px',
    full: '9999px',
  },
  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)',
  },
  breakpoints: {
    sm: '576px',
    md: '768px',
    lg: '992px',
    xl: '1200px',
  },
}

使用 ThemeProvider #

jsx
import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'

const Button = styled.button`
  padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
  background-color: ${props => props.theme.colors.primary};
  color: ${props => props.theme.colors.white};
  border: none;
  border-radius: ${props => props.theme.borderRadius.sm};
  font-size: ${props => props.theme.fontSizes.md};
  cursor: pointer;
  
  &:hover {
    opacity: 0.9;
  }
`

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>Click me</Button>
    </ThemeProvider>
  )
}

深色模式 #

定义双主题 #

jsx
const lightTheme = {
  colors: {
    primary: '#007bff',
    background: '#ffffff',
    surface: '#f8f9fa',
    text: '#333333',
    textMuted: '#6c757d',
    border: '#dee2e6',
  },
  mode: 'light',
}

const darkTheme = {
  colors: {
    primary: '#4da3ff',
    background: '#1a1a1a',
    surface: '#2d2d2d',
    text: '#ffffff',
    textMuted: '#adb5bd',
    border: '#495057',
  },
  mode: 'dark',
}

主题切换组件 #

jsx
import { useState } from 'react'
import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'

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

const ToggleButton = styled.button`
  position: fixed;
  top: 20px;
  right: 20px;
  padding: 10px 20px;
  background-color: ${props => props.theme.colors.primary};
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
`

function App() {
  const [isDark, setIsDark] = useState(false)
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <Container>
        <ToggleButton onClick={() => setIsDark(!isDark)}>
          {isDark ? '☀️ Light' : '🌙 Dark'}
        </ToggleButton>
        <h1>Hello World</h1>
        <p>Current mode: {isDark ? 'Dark' : 'Light'}</p>
      </Container>
    </ThemeProvider>
  )
}

跟随系统主题 #

jsx
import { useState, useEffect } from 'react'
import { ThemeProvider } from '@emotion/react'

function App() {
  const [isDark, setIsDark] = useState(() => {
    if (typeof window !== 'undefined') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
    }
    return false
  })
  
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    const handleChange = (e) => setIsDark(e.matches)
    
    mediaQuery.addEventListener('change', handleChange)
    return () => mediaQuery.removeEventListener('change', handleChange)
  }, [])
  
  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <AppContent />
    </ThemeProvider>
  )
}

useTheme Hook #

访问主题 #

在组件中使用 useTheme Hook 访问主题:

jsx
/** @jsxImportSource @emotion/react */
import { css, useTheme } from '@emotion/react'

function Card({ children }) {
  const theme = useTheme()
  
  return (
    <div
      css={css`
        padding: ${theme.spacing.md};
        background-color: ${theme.colors.surface};
        border-radius: ${theme.borderRadius.md};
        box-shadow: ${theme.shadows.md};
      `}
    >
      {children}
    </div>
  )
}

条件样式 #

根据主题模式调整样式:

jsx
/** @jsxImportSource @emotion/react */
import { css, useTheme } from '@emotion/react'

function ThemedInput({ ...props }) {
  const theme = useTheme()
  
  return (
    <input
      css={css`
        padding: ${theme.spacing.sm} ${theme.spacing.md};
        background-color: ${theme.mode === 'dark' ? '#2d2d2d' : '#ffffff'};
        color: ${theme.colors.text};
        border: 1px solid ${theme.colors.border};
        border-radius: ${theme.borderRadius.sm};
        outline: none;
        
        &:focus {
          border-color: ${theme.colors.primary};
          box-shadow: 0 0 0 3px ${theme.colors.primary}33;
        }
      `}
      {...props}
    />
  )
}

主题扩展 #

创建变体主题 #

jsx
const createTheme = (baseTheme, overrides) => ({
  ...baseTheme,
  ...overrides,
  colors: {
    ...baseTheme.colors,
    ...overrides.colors,
  },
})

const blueTheme = createTheme(lightTheme, {
  colors: {
    primary: '#007bff',
  },
})

const greenTheme = createTheme(lightTheme, {
  colors: {
    primary: '#28a745',
  },
})

const purpleTheme = createTheme(lightTheme, {
  colors: {
    primary: '#6f42c1',
  },
})

嵌套主题 #

主题可以嵌套,内层主题会覆盖外层主题:

jsx
import { ThemeProvider } from '@emotion/react'
import styled from '@emotion/styled'

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

function App() {
  return (
    <ThemeProvider theme={blueTheme}>
      <Button>Blue Button</Button>
      
      <ThemeProvider theme={greenTheme}>
        <Button>Green Button</Button>
      </ThemeProvider>
      
      <Button>Blue Button Again</Button>
    </ThemeProvider>
  )
}

TypeScript 支持 #

主题类型定义 #

typescript
declare module '@emotion/react' {
  export interface Theme {
    colors: {
      primary: string
      secondary: string
      success: string
      danger: string
      warning: string
      info: string
      background: string
      surface: string
      text: string
      textMuted: string
      border: string
    }
    fontSizes: {
      xs: string
      sm: string
      md: string
      lg: string
      xl: string
    }
    spacing: {
      xs: string
      sm: string
      md: string
      lg: string
      xl: string
    }
    borderRadius: {
      sm: string
      md: string
      lg: string
      full: string
    }
    shadows: {
      sm: string
      md: string
      lg: string
    }
    mode: 'light' | 'dark'
  }
}

类型安全的主题使用 #

tsx
import styled from '@emotion/styled'
import { Theme } from '@emotion/react'

const Button = styled.button<{ variant?: keyof Theme['colors'] }>`
  background-color: ${props => props.theme.colors[props.variant || 'primary']};
  color: white;
  padding: ${props => props.theme.spacing.sm} ${props => props.theme.spacing.md};
  border: none;
  border-radius: ${props => props.theme.borderRadius.sm};
`

主题工具函数 #

响应式工具 #

jsx
const media = (breakpoint) => (styles) => css`
  @media (min-width: ${breakpoint}) {
    ${styles}
  }
`

const ResponsiveText = styled.p`
  font-size: ${props => props.theme.fontSizes.sm};
  
  ${media(props => props.theme.breakpoints.md)(css`
    font-size: ${props => props.theme.fontSizes.md};
  `)}
  
  ${media(props => props.theme.breakpoints.lg)(css`
    font-size: ${props => props.theme.fontSizes.lg};
  `)}
`

颜色工具 #

jsx
const lighten = (color, percent) => {
  const num = parseInt(color.replace('#', ''), 16)
  const amt = Math.round(2.55 * percent)
  const R = (num >> 16) + amt
  const G = (num >> 8 & 0x00FF) + amt
  const B = (num & 0x0000FF) + amt
  return '#' + (
    0x1000000 +
    (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
    (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
    (B < 255 ? (B < 1 ? 0 : B) : 255)
  ).toString(16).slice(1)
}

const Button = styled.button`
  background-color: ${props => props.theme.colors.primary};
  
  &:hover {
    background-color: ${props => lighten(props.theme.colors.primary, 10)};
  }
`

最佳实践 #

1. 主题文件组织 #

text
src/
  themes/
    index.ts
    light.ts
    dark.ts
    types.ts
    utils.ts

2. 默认主题 #

始终提供默认主题:

jsx
import { ThemeProvider } from '@emotion/react'

const defaultTheme = {
  colors: { ... },
  ...
}

function App({ theme = defaultTheme }) {
  return (
    <ThemeProvider theme={theme}>
      <AppContent />
    </ThemeProvider>
  )
}

3. 主题持久化 #

将主题选择保存到 localStorage:

jsx
import { useState, useEffect } from 'react'

function useTheme() {
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem('theme')
    return saved ? JSON.parse(saved) : lightTheme
  })
  
  useEffect(() => {
    localStorage.setItem('theme', JSON.stringify(theme))
  }, [theme])
  
  return [theme, setTheme]
}

下一步 #

掌握了主题定制后,继续学习 全局样式,了解如何添加全局 CSS 样式。

最后更新:2026-03-28