优先读 localStorage,无值则 fallback 到 matchMedia,页面加载时立即设置 document.documentElement.dataset.theme 并内联预设 CSS 变量,确保无 FOUC;切换时同步更新 dataset、localStorage、meta[name="color-scheme"],并控制媒体查询监听逻辑。

直接改 document.documentElement.dataset.theme,配合 localStorage 和 matchMedia 监听,别只靠 @media (prefers-color-scheme: dark) ——它不会响应你点的按钮。
怎么初始化主题:优先读 localStorage, fallback 到系统偏好
页面一加载就要决定用什么主题,否则会闪白底(FOUC)。不能等 JS 执行完才设样式。
- 先查
localStorage.getItem('theme'),有值就用它(比如"dark"或"light") - 没值再查
window.matchMedia('(prefers-color-scheme: dark)').matches,true 就设data-theme="dark" - 必须在
里用内联预设初始变量,例如:html[data-theme="dark"] { --bg: #121212; --text: #e0e0e0; } - 别用
class="dark"初始化 body——data-theme更语义化、不干扰已有 class(比如 Tailwind 的bg-white)
怎么写 CSS:用 :root + data-theme 覆盖,别写两套独立样式
CSS 变量是夜间模式可维护的核心。写死颜色或复制整套规则,后期改一个色就得搜十次。
- 在
:root定义默认值::root { --bg: #fff; --text: #1a1a1a; } - 用属性选择器覆盖:
html[data-theme="dark"] { --bg: #121212; --text: #e0e0e0; } - 所有组件用
background: var(--bg)、color: var(--text),不用.dark .card这种嵌套写法 - 记得加
html前缀:html[data-theme="dark"],否则可能被子元素样式覆盖
怎么绑定切换逻辑:同步三处,且手动操作要压倒自动监听
用户点一次按钮,必须立刻生效、持久保存、并阻止系统监听“偷偷翻盘”。
- 点击按钮时调用统一函数:
function setTheme(theme) { document.documentElement.dataset.theme = theme; localStorage.setItem('theme', theme); } - 监听系统变化时,只在
themeMode === 'auto'时才更新:const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', e => { if (themeMode === 'auto') setTheme(e.matches ? 'dark' : 'light'); }); - 手动切换后,把
themeMode设为'light'或'dark',并移除mediaQuery监听器 - 按钮状态必须实时同步:
btn.setAttribute('aria-pressed', currentTheme === 'dark'),别只靠 class 判断
容易漏掉的关键点:meta[name="color-scheme"] 和无障碍反馈
安卓 Chrome 和 Safari 会读 控制地址栏、表单控件颜色。JS 切了主题但忘了它,UI 就割裂。
- 每次
setTheme()里都要更新:document.querySelector('meta[name="color-scheme"]').content = theme === 'dark' ? 'light dark' : 'light'; - 按钮文案要随主题变:
btn.textContent = currentTheme === 'dark' ? '切换到日间' : '切换到夜间' - 首次加载时如果系统是暗色但用户从没点过按钮,
localStorage是空的——这时候必须显式设data-theme,否则html[data-theme="dark"]规则压根不生效











