本文约 5,092 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:读者在深夜被你的白底晃到眼睛
做技术博客的人自己大概都有这个习惯:晚上把系统切成深色,然后打开一篇文章——页面雪白,亮度直接刺眼。于是有人装插件,有人换主题。但插件方案往往带来额外请求和一堆用不到的 JS,主题自带的深色模式又多半只做了一半:能跟随系统,但不能手动切换;或者能切换,刷新后又回到亮色。
深色模式看起来只是换个配色,实现上有三个必须解决的细节:一是首屏闪烁(页面先白一下再变黑),二是状态持久化(用户的选择要跨页面、跨会话记住),三是后台编辑器同步(编辑时看到的样式要跟前台一致,否则排完版发布后才发现深色下对比度不对)。本文用纯 CSS + 少量原生 JS 解决前两点,用 WordPress 自带的主题支持解决第三点。若你还没处理后台编辑样式,可以先看 主题与区块编辑器的样式同步(editor-style)。
原理分析:两套信号源如何合成一个最终主题
深色模式的信号源有两个:操作系统偏好(prefers-color-scheme)和用户的显式选择。二者必须有明确的优先级——显式选择优先,没有显式选择时才跟随系统。
实现方式上,最稳妥的模型是「根元素上打标记」:在 <html> 上加 data-theme="dark" 或 light,CSS 用属性选择器写两套变量。跟随系统的部分用 @media (prefers-color-scheme: dark) 兜底,手动切换时用 JS 改写 data-theme 并写入 localStorage。
| 方案 | 优点 | 缺点 |
|---|---|---|
纯 prefers-color-scheme |
零 JS | 用户无法自主选择 |
data-theme + JS |
可手动切换、可持久化 | 需要防闪烁脚本 |
| 两套独立样式表 | 结构清晰 | 首屏可能加载两份 CSS |
| 插件方案 | 开箱即用 | 额外请求、样式冲突难调 |
首屏闪烁的成因是执行顺序:主题色在 JS 读取 localStorage 后才生效,而 JS 通常在文档解析后期才执行,中间那段时间是默认亮色。解决办法是把一小段读取 localStorage 的脚本内联在 <head> 最前面,同步执行、立即给 <html> 打标记,浏览器首次绘制时就已经是正确配色。
还有两个 CSS 属性值得一提:color-scheme: light dark 会让浏览器把表单控件、滚动条等「UA 默认样式」也切换到深色;<meta name="theme-color"> 配合 media 属性能让移动端浏览器地址栏跟着变色。这两处不做,深色页面会拖着一条亮色的滚动条和地址栏,非常出戏。
保姆级实操
- 定义配色变量:在
:root写亮色变量,在[data-theme="dark"]和@media (prefers-color-scheme: dark)里覆盖成深色。 - 加 color-scheme:让表单控件与滚动条跟随。
- 写防闪烁内联脚本:在
wp_head优先级 0 输出,同步读取 localStorage 并给<html>加属性。 - 做切换按钮:按钮放在头部,点击时切换属性、写 localStorage、更新
aria-pressed。 - 补 theme-color:用
wp_head输出两条带 media 的 meta。 - 后台编辑器同步:启用
dark-editor-style主题支持并注册深色编辑器样式表。
:root {
--bg: #ffffff;
--text: #1f2328;
--muted: #656d76;
--border: #d0d7de;
--accent: #0969da;
color-scheme: light;
}
@media ( prefers-color-scheme: dark ) {
:root:not( [data-theme="light"] ) {
--bg: #0d1117;
--text: #e6edf3;
--muted: #8b949e;
--border: #30363d;
--accent: #58a6ff;
color-scheme: dark;
}
}
:root[ data-theme="dark" ] {
--bg: #0d1117;
--text: #e6edf3;
--muted: #8b949e;
--border: #30363d;
--accent: #58a6ff;
color-scheme: dark;
}
body {
background: var( --bg );
color: var( --text );
}
<?php
// 子主题 functions.php:首屏防闪烁,必须最早执行
add_action( 'wp_head', 'mimangfei_theme_mode_bootstrap', 0 );
function mimangfei_theme_mode_bootstrap() {
?>
<script>
(function () {
var d = document.documentElement;
try {
var saved = localStorage.getItem( 'mimangfei-theme' );
if ( saved === 'dark' || saved === 'light' ) {
d.setAttribute( 'data-theme', saved );
}
} catch ( e ) {}
}());
</script>
<?php
}
// 移动端地址栏跟随配色
add_action( 'wp_head', 'mimangfei_theme_color_meta', 2 );
function mimangfei_theme_color_meta() {
echo '<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">' . "\n";
echo '<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)">' . "\n";
}
// 切换按钮逻辑:保存在 localStorage,跨页面生效
const btn = document.querySelector( '[data-theme-toggle]' );
if ( btn ) {
const root = document.documentElement;
const saved = localStorage.getItem( 'mimangfei-theme' );
const initial = saved || ( matchMedia( '(prefers-color-scheme: dark)' ).matches ? 'dark' : 'light' );
root.setAttribute( 'data-theme', initial );
btn.setAttribute( 'aria-pressed', String( initial === 'dark' ) );
btn.addEventListener( 'click', () => {
const next = root.getAttribute( 'data-theme' ) === 'dark' ? 'light' : 'dark';
root.setAttribute( 'data-theme', next );
btn.setAttribute( 'aria-pressed', String( next === 'dark' ) );
try {
localStorage.setItem( 'mimangfei-theme', next );
} catch ( e ) {}
} );
}
<?php
// 6. 后台编辑器也要能切深色,避免排版时看不到真实效果
add_action( 'after_setup_theme', 'mimangfei_dark_editor_support' );
function mimangfei_dark_editor_support() {
add_theme_support( 'editor-styles' );
add_theme_support( 'dark-editor-style' );
add_editor_style( 'assets/css/editor-style.css' );
add_editor_style( 'assets/css/editor-style-dark.css' );
}
常见踩坑
- 切换脚本放在页脚:等到
wp_footer才执行,首屏已经白过一次。防闪烁脚本必须在<head>里同步执行。 - 只用
@media不做手动切换:用户系统设成亮色但想在你的站点用深色时无从下手,这是最常见的投诉来源。 - 案例:某站上线深色模式后,用户反馈「刷新后闪一下白色」。排查发现防闪烁脚本挂在
wp_head的默认优先级 10,而主题的wp_enqueue_style之前还有缓存插件输出的样式,导致脚本实际位置靠后。改成优先级 0 后闪烁消失。另外他们把 localStorage 的读写放在 try/catch 之外,Safari 无痕模式下抛异常导致后续脚本全部中断,加上 try/catch 才稳定。 - 图片在深色背景下刺眼:纯白底的截图、流程图在深色模式下非常突兀。用 CSS 给
.entry-content img加深色下的filter: brightness(0.9)或加一层浅色圆角背景。 - 对比度不达标:深色下把正文设成
#666会低于 WCAG AA 的 4.5:1。正文与背景的对比度必须实测,别凭感觉调色。 - 代码高亮插件配色不跟随:多数高亮插件有自己的固定配色,深色模式下代码块仍是白底。要么选支持两套主题的插件,要么手动给
pre写深色规则。 - 缓存插件按 URL 缓存 HTML:如果深色模式靠服务端输出不同的
<html>属性,会被全页缓存串味。把状态判断放在客户端 JS,缓存才安全。 color-scheme只写在body上:滚动条和表单控件的 UA 样式跟随的是根元素,写在 body 上对部分控件无效。写在:root。
总结
深色模式的判断标准是三件事:跟得上系统、切得手动、刷新不闪。三者都做到才算合格。行动清单:CSS 变量化配色 → :root 加 color-scheme → wp_head 内联防闪烁脚本 → 切换按钮写 localStorage → 补 theme-color meta → 启用 dark-editor-style 让后台同步 → 逐页检查对比度与图片观感。做完这套之后,可以顺手把 主题对文章内区块的默认样式美化 一起收拾,因为引用块、表格、代码块在深色下的默认配色几乎都要单独调。
