搜索全站
主题 13 分钟阅读

WordPress 主题深色模式实现:prefers-color-scheme 与手动切换

深色模式只跟随系统不够用,还要能手动切。本文给出 prefers-color-scheme 自动适配、localStorage 记住选择、首屏防闪烁内联脚本与后台编辑器深色样式的完整代码,附 theme-color 设置。

U
UFO_PF 2024-04-03 · 阅读 13 分钟
Wo 主题
阅读提示
本文约 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 属性能让移动端浏览器地址栏跟着变色。这两处不做,深色页面会拖着一条亮色的滚动条和地址栏,非常出戏。

保姆级实操

  1. 定义配色变量:在 :root 写亮色变量,在 [data-theme="dark"] 和 @media (prefers-color-scheme: dark) 里覆盖成深色。
  2. 加 color-scheme:让表单控件与滚动条跟随。
  3. 写防闪烁内联脚本:在 wp_head 优先级 0 输出,同步读取 localStorage 并给 <html> 加属性。
  4. 做切换按钮:按钮放在头部,点击时切换属性、写 localStorage、更新 aria-pressed。
  5. 补 theme-color:用 wp_head 输出两条带 media 的 meta。
  6. 后台编辑器同步:启用 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' );
}

常见踩坑

  1. 切换脚本放在页脚:等到 wp_footer 才执行,首屏已经白过一次。防闪烁脚本必须在 <head> 里同步执行。
  2. 只用 @media 不做手动切换:用户系统设成亮色但想在你的站点用深色时无从下手,这是最常见的投诉来源。
  3. 案例:某站上线深色模式后,用户反馈「刷新后闪一下白色」。排查发现防闪烁脚本挂在 wp_head 的默认优先级 10,而主题的 wp_enqueue_style 之前还有缓存插件输出的样式,导致脚本实际位置靠后。改成优先级 0 后闪烁消失。另外他们把 localStorage 的读写放在 try/catch 之外,Safari 无痕模式下抛异常导致后续脚本全部中断,加上 try/catch 才稳定。
  4. 图片在深色背景下刺眼:纯白底的截图、流程图在深色模式下非常突兀。用 CSS 给 .entry-content img 加深色下的 filter: brightness(0.9) 或加一层浅色圆角背景。
  5. 对比度不达标:深色下把正文设成 #666 会低于 WCAG AA 的 4.5:1。正文与背景的对比度必须实测,别凭感觉调色。
  6. 代码高亮插件配色不跟随:多数高亮插件有自己的固定配色,深色模式下代码块仍是白底。要么选支持两套主题的插件,要么手动给 pre 写深色规则。
  7. 缓存插件按 URL 缓存 HTML:如果深色模式靠服务端输出不同的 <html> 属性,会被全页缓存串味。把状态判断放在客户端 JS,缓存才安全。
  8. color-scheme 只写在 body 上:滚动条和表单控件的 UA 样式跟随的是根元素,写在 body 上对部分控件无效。写在 :root。

总结

深色模式的判断标准是三件事:跟得上系统、切得手动、刷新不闪。三者都做到才算合格。行动清单:CSS 变量化配色 → :root 加 color-scheme → wp_head 内联防闪烁脚本 → 切换按钮写 localStorage → 补 theme-color meta → 启用 dark-editor-style 让后台同步 → 逐页检查对比度与图片观感。做完这套之后,可以顺手把 主题对文章内区块的默认样式美化 一起收拾,因为引用块、表格、代码块在深色下的默认配色几乎都要单独调。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。