📚 现代前端开发 目录
阶段4:UI与体验
第 24 / 35 课

暗色模式

优雅实现深色主题

📖 核心概念

💻 代码实现

暗色模式实现 ✅ css
/* ===== 三层颜色系统 ===== */
/* 层1: 原始色值 */
:root {
  --gray-50: #f8fafc; --gray-100: #f1f5f9; --gray-900: #0f172a;
  --cyan-500: #06b6d4; --cyan-600: #0891b2;
}

/* 层2: 语义Token(亮色) */
:root {
  --color-bg-base: var(--gray-50);
  --color-bg-surface: white;
  --color-bg-elevated: var(--gray-100);
  --color-bg-sunken: var(--gray-100);
  --color-text-primary: var(--gray-900);
  --color-text-secondary: #64748b;
  --color-border-default: #e2e8f0;
  --color-accent: var(--cyan-500);
  --color-accent-hover: var(--cyan-600);
  --shadow-card: 0 1px 3px rgba(0,0,0,0.1);
  --shadow-elevated: 0 10px 25px rgba(0,0,0,0.1);
}

/* 层2: 语义Token(暗色) */
[data-theme="dark"] {
  --color-bg-base: var(--gray-900);
  --color-bg-surface: #1e293b;
  --color-bg-elevated: #334155;
  --color-bg-sunken: #020617;
  --color-text-primary: #f1f5f9;
  --color-text-secondary: #94a3b8;
  --color-border-default: #334155;
  --color-accent: var(--cyan-500);
  --color-accent-hover: var(--cyan-400);
  --shadow-card: 0 1px 3px rgba(0,0,0,0.3);
  --shadow-elevated: 0 10px 25px rgba(0,0,0,0.4);
}

/* 自动跟随系统 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --color-bg-base: var(--gray-900);
    --color-bg-surface: #1e293b;
    /* ... 暗色值 */
  }
}

/* ===== 防止闪烁 ===== */
/* 在<head>中内联此脚本 */
/*
<script>
  (function() {
    const stored = localStorage.getItem('theme');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const theme = stored || (prefersDark ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', theme);
  })();
</script>
*/
React暗色模式系统 ✅ tsx
import { useEffect, useState, useCallback } from 'react';

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

function useTheme() {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const resolvedTheme: ResolvedTheme = (() => {
    if (theme !== 'system') return theme;
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  })();

  const setTheme = useCallback((newTheme: Theme) => {
    setThemeState(newTheme);
    localStorage.setItem('theme', newTheme);
    const resolved = newTheme === 'system'
      ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
      : newTheme;
    document.documentElement.setAttribute('data-theme', resolved);
  }, []);

  // 监听系统主题变化
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = (e: MediaQueryListEvent) => {
      if (theme === 'system') {
        document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
      }
    };
    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [theme]);

  return { theme, resolvedTheme, setTheme };
}

// 主题切换按钮
function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  const isDark = resolvedTheme === 'dark';

  return (
    <button
      onClick={() => setTheme(isDark ? 'light' : 'dark')}
      className="theme-toggle"
      aria-label={isDark ? '切换到亮色模式' : '切换到暗色模式'}
    >
      {/* 太阳/月亮图标,带平滑过渡 */}
      <div className="theme-icon" style={{ transform: isDark ? 'rotate(0)' : 'rotate(180deg)' }}>
        {isDark ? '🌙' : '☀️'}
      </div>
    </button>
  );
}

/* 暗色模式图片适配 */
/* .dark-image { display: none; }
[data-theme="dark"] .light-image { display: none; }
[data-theme="dark"] .dark-image { display: block; }

/* 暗色模式阴影调整 */
[data-theme="dark"] .card {
  box-shadow: 0 1px 3px rgba(0,0,0,0.4); /* 暗色下更强的阴影 */
  border: 1px solid var(--color-border-default); /* 暗色下用边框代替阴影 */
} */

🎯 练习任务

🏆 成就解锁
暗色模式专家 — 掌握暗色模式实现技巧,打造优雅的双主题体验