Front-End-Checklist 深色模式实践:用 prefers-color-scheme 与 CSS 自定义属性构建可维护的主题系统
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
本篇指南围绕 Front-End-Checklist 仓库中的
dark-mode-css规则展开,讲解如何用prefers-color-scheme媒体查询 + CSS 自定义属性(Custom Properties)实现自动适配系统偏好的深色模式,并在此基础上支持用户手动切换、原生控件配色、图片降亮与平滑过渡。读完本文,你将掌握一套可直接落地的语义化设计令牌(design token)主题方案,并能用仓库提供的检查清单与源码证据完成自查。
规则背景:为什么深色模式是前端必备能力
Front-End-Checklist 将dark-mode-css归类为css/design-tokens子类目下的规则(见 规则元数据),优先级为 medium、难度为 intermediate、预计耗时 25 分钟。规则的核心主张是:
深色模式的最佳实现方式是:在媒体查询或
[data-theme]选择器中重新定义 CSS 自定义属性的值,而不改动任何组件样式。
这条规则依赖的底层能力来自两条相邻规则:
- css-custom-properties:把颜色、间距、字体等设计系统值统一定义在
:root上,深色模式本质上是"在媒体查询里重新定义自定义属性的值"; - color-oklch:与 dark-mode-css 同属
css/design-tokens区域,两者常被一起评审——用感知均匀的 oklch 色彩空间构建色板,能显著降低手工搭建深色模式色阶的难度。
规则文档的完整内容存放于 skills/dark-mode-css/SKILL.md(Agent 使用入口)与其 references/rule.md(完整实现细节)。
快速参考:四条核心结论
- 使用
@media (prefers-color-scheme: dark)应用深色模式样式; - 将浅色/深色颜色定义为
:root上的 CSS 自定义属性,便于一键切换; - 支持手动切换:把偏好存入
localStorage,并在根元素上设置data-theme属性; - 确保深色模式的配色满足 WCAG 对比度要求。
为什么值得做:需求动因与工程价值
超过一半的用户出于减轻视疲劳的目的偏好深色模式,尤其是在低光照环境下;对光敏感(photosensitivity)的用户更是依赖它。通过prefers-color-scheme实现深色模式,尊重用户操作系统层面的偏好,用户无需在站点内寻找开关;而 CSS 自定义属性让这一切成为"干净的、可维护的增量",而非一次破坏性重构。
用自定义属性实现主题化的关键工程价值在于:一次定义、处处生效。硬编码颜色散落在各个 CSS 文件里,一次换肤(rebrand)、一次深色模式适配、一次间距调整,都要逐个查找和修改几十行代码;自定义属性把值集中起来,一处变更全局传播,并且支持运行时主题(runtime theming)——这是 Sass/Less 这类预处理器变量做不到的(见 css-custom-properties 规则的 Why It Matters)。
核心实现:语义化令牌 + 媒体查询重定义
第一步:定义语义化颜色令牌,而不是"dark-blue"
正确做法是定义语义化的令牌名称——不是--color-dark-blue这种描述具体外观的名字,而是--color-primary、--color-surface这类描述用途的名字。这样深色模式只需要重定义同一组变量:
/* ✅ Define semantic color tokens — not "dark-blue" but "color-primary" */ :root { /* Light mode values (default) */ --color-surface: #ffffff; --color-surface-elevated: #f9fafb; --color-text: #111827; --color-text-muted: #6b7280; --color-border: #e5e7eb; --color-primary: #2563eb; --color-primary-foreground: #ffffff; --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05); } /* Dark mode: just redefine the same variables */ @media (prefers-color-scheme: dark) { :root { --color-surface: #0f172a; --color-surface-elevated: #1e293b; --color-text: #f1f5f9; --color-text-muted: #94a3b8; --color-border: #334155; --color-primary: #3b82f6; --color-primary-foreground: #ffffff; --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3); } } /* Components use variables — no dark-mode-specific component CSS needed */ .card { background: var(--color-surface-elevated); border: 1px solid var(--color-border); color: var(--color-text); box-shadow: var(--shadow-sm); }这套代码中值得注意的设计要点:
- 默认值即浅色模式:
:root中默认就是浅色值,不写媒体查询时页面也能正常渲染; - 深色模式只重定义变量:组件
.card始终使用var(...),因此不需要任何深色模式专属的组件 CSS; - 阴影也纳入令牌体系:深色模式下阴影透明度从
0.05提到0.3,因为深色背景上的阴影几乎不可见,需要更强的深度表现。
第二步:让颜色系统与"感知"对齐(进阶)
如果进一步采用 oklch 色彩空间(见 color-oklch 规则),深色模式只需要调整**明度轴(Lightness)**即可获得感知一致的色阶:
/* Dark mode — adjust only the lightness axis */ @media (prefers-color-scheme: dark) { :root { --color-text: var(--color-neutral-50); --color-text-muted: var(--color-neutral-400); --color-bg: var(--color-neutral-950); --color-bg-surface: var(--color-neutral-900); --color-border: var(--color-neutral-800); } }原因在于 sRGB 色彩空间不具备感知均匀性——10% 的明度变化在不同色相上看起来差异巨大;而 oklch 的 L 通道在任何色相/彩度下都产生相同的感知亮度变化,这使它特别适合构建深色模式色板和一致的 hover/active 状态。
支持手动切换:data-theme + localStorage
prefers-color-scheme尊重系统偏好,但有些用户希望站点内独立控制。规则给出的方案是:用data-theme属性覆盖操作系统偏好,并用localStorage持久化。
CSS 侧:属性选择器定义覆盖值
/* Allow>// Manual toggle with localStorage persistence function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme) localStorage.setItem('theme-preference', theme) } // On load — respect saved preference or OS default const saved = localStorage.getItem('theme-preference') if (saved) { document.documentElement.setAttribute('data-theme', saved) } // If no saved preference, the media query handles it automatically这里的关键设计是优先级分层:用户显式保存的偏好 > 系统偏好。没有保存过偏好的用户,媒体查询会自动接管——两种路径互不冲突。
仓库源码印证:matchMedia 检测偏好
本仓库中 Front-End-Checklist 站点自身就提供了用户偏好检测的参考实现,见 apps/web/lib/accessibility/preferences.ts:
/** Returns the user's preferred color scheme ('light', 'dark', or 'no-preference'). */ export function prefersColorScheme(): 'light' | 'dark' | 'no-preference' { if (typeof window === 'undefined') return 'no-preference' if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark' if (window.matchMedia('(prefers-color-scheme: light)').matches) return 'light' return 'no-preference' }同一文件还提供了prefersReducedMotion()(对应prefers-reduced-motion)与prefersHighContrast()(对应prefers-contrast: more),说明前端偏好检测是一个统一的工具层话题,深色模式只是其中之一。另外,站点在 apps/web/app/layout.tsx 中通过 Next.js 的viewport.themeColor配置,让浏览器地址栏/主题色随prefers-color-scheme自动切换:
export const viewport: Viewport = { width: 'device-width', initialScale: 1, maximumScale: 5, themeColor: [ { media: '(prefers-color-scheme: light)', color: '#ffffff' }, { media: '(prefers-color-scheme: dark)', color: '#09090b' } ] }原生控件的配色:color-scheme 属性
很多开发者只处理了页面背景和文字颜色,却忽略了滚动条、表单输入框、日期选择器等浏览器原生控件——它们在深色页面下会保持刺眼的白色。color-scheme属性告诉浏览器使用深色原生控件:
:root { color-scheme: light dark; /* Browser adjusts scrollbars, form inputs, etc */ } [data-theme="dark"] { color-scheme: dark; }:root { color-scheme: light dark; }表示同时允许两种配色,浏览器根据当前系统偏好渲染对应原生控件;[data-theme="dark"] { color-scheme: dark; }在用户手动切到深色时强制使用深色原生控件。
图片在深色模式下的降亮处理
截图和示意图通常基于浅色背景制作,直接放到深色页面上会显得刺眼。规则给出了一个简单有效的降亮方案:
/* Reduce image brightness in dark mode (useful for screenshots and diagrams) */ @media (prefers-color-scheme: dark) { img:not([src*=".svg"]) { filter: brightness(0.85) contrast(1.05); } }这里特意排除了 SVG(img:not([src*=".svg"])),因为 SVG 常作为图标使用,其颜色应当跟随当前主题令牌,而非被统一降亮。
平滑过渡:切换主题不闪变
主题切换瞬间"跳变"会让人感到生硬,给颜色类属性加上过渡即可:
/* Add a smooth transition when switching themes */ :root { transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease; } /* But skip transition on page load */ .no-transition * { transition: none !important; }- 只对
background-color、color、border-color做过渡——这些都是合成友好的属性,不会触发布局抖动(相关规则见 animation-performance 同类思路:颜色过渡用 CSS transitions 而非引发布局变更的手段); - 首次加载时给根元素加上
.no-transition类,避免页面初始化时出现一次"从浅色过渡到深色"的闪烁动画。
检查清单:如何用 SKILL 进行代码评审
SKILL.md(见 skills/dark-mode-css/SKILL.md)定义了四个评审动作,适用于人工评审或 Agent 驱动的检查:
| 动作 | 内容 |
|---|---|
| Check | 检查 CSS 是否支持深色模式:查找prefers-color-scheme用法或data-theme属性模式;识别任何在深色模式下会显示异常的硬编码颜色 |
| Fix | 将颜色提取为 CSS 自定义属性,并在prefers-color-scheme: dark媒体查询内重新定义它们 |
| Explain | 解释如何用 CSS 自定义属性、prefers-color-scheme媒体查询和 JS 开关实现深色模式 |
| Code Review | 审查样式表、组件样式与响应式状态,在渲染出的 UI 中精确定位违反规则的 selector、声明或断点 |
评审时重点关注:是否存在绕过令牌直接硬编码的颜色值?组件是否在媒体查询中重复写了整套深色样式(而不是只重定义变量)?手动开关与系统偏好是否形成了正确的优先级?
验证清单:上线前逐项确认
规则的 Verification 部分(见 references/rule.md)要求:
- 在规则影响的断点和交互状态下检查渲染出的 UI;
- 在 DevTools 中确认计算样式(computed styles)与预期修复一致;
- 上线前至少在一个移动端视口和一个桌面端视口下测试;
- 如果规则影响动效、对比度或布局稳定性,直接验证这些面向用户的结果。
另外结合 SKILL 元数据(category: css,estimatedTime: 25 分钟)与规则正文,建议补充以下实操检查:
- 对比度验证:深色模式下文字/背景对需满足 WCAG 2.1 对比度要求(普通文本 4.5:1,大文本 3:1)。注意 oklch 的 L 通道与 WCAG 相对亮度不是一回事,仍需用对比度检查工具实测(见 color-oklch 的对比度说明);
- FOUC 预防:手动切换场景下,若 JS 在首屏后才设置
data-theme,用户可能短暂看到错误的主题——可考虑内联一段首屏脚本(如同 preferences.ts 的检测逻辑)尽早恢复偏好; - 两种路径都测:既测试跟随系统偏好(不设置任何存储值),也测试手动覆盖(写入
localStorage后刷新)。
总结
深色模式不是一个"加一个 media query 改几个颜色"的简单活,而是一次设计系统层面的治理机会。Front-End-Checklist 的dark-mode-css规则给出的完整路径是:语义化令牌(自定义属性)→ 媒体查询/属性选择器重定义 → localStorage 持久化 → color-scheme 处理原生控件 → 图片降亮 → 平滑过渡 → 多视口验证。这套方案既尊重系统偏好(prefers-color-scheme),又允许用户手动覆盖(data-theme),同时把组件样式与主题色彻底解耦,是值得在任意现代 Web 项目中直接复用的架构模式。
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考