Decentralization? We're still early!

如何让 WordPress Twenty Twenty-Five 主题支持暗色模式

  • 如何让 WordPress Twenty Twenty-Five 主题支持暗色模式

    發布人 Brave 2026-03-04 06:23

    本文基于对 WordPress Twenty Twenty-Five 主题 v1.4 全部源码的逆向分析,以及基于此分析实际编写的 TT5 Dark Mode 插件。所有技术结论均可溯源至具体文件与代码行。

    当我解压 Twenty Twenty-Five(以下简称 TT5)的主题包时,我以为会找到某种暗色模式的半成品——毕竟这是 2025 年了。结果是一个违反直觉的发现:

    • JavaScript 文件。整个主题没有一行 JS。
    • prefers-color-scheme 媒体查询。CSS 和 JSON 中均无。
    • 暗色模式切换逻辑。functions.php 仅 167 行,干净得像一张白纸。

    但同时,我发现了另一个事实:TT5 已经为暗色模式准备了完整的设计素材——4 套经过精心设计的深色调色板(Evening、Twilight、Midnight、Sunrise),作为 Style Variations 静静地躺在 styles/colors/ 目录里。

    颜料已经调好,画布已经绑好,画笔已经摆好。只是没有人把它们组装起来交给访客。

    这不是疏忽。这是架构的边界。


    一、官方的克制不是懒惰,是结构性困境

    Twenty Twenty-One 的实验与退场

    2021 年,WordPress 在 Twenty Twenty-One 中做过一次大胆尝试:内置 Dark Mode Support 复选框,通过 prefers-color-scheme 媒体查询实现系统级自动切换。那是经典主题(Classic Theme)时代的产物——所有样式写在 style.css 里,你可以在 CSS 中为所欲为。

    但从 Twenty Twenty-Two 开始,WordPress 转向了 Block Theme(FSE 全站编辑主题),核心样式的定义权从 style.css 移交给了 theme.json。这一转向改变了一切。

    theme.json 的根本性限制

    theme.json 是一个 静态 JSON 配置文件。它能定义颜色、间距、排版——但它不能写条件逻辑。没有 if,没有 @media,没有 prefers-color-scheme

    TT5 的 theme.json(v3 schema,对应 WordPress 6.7+)中,颜色是这样定义的:

    {
      "settings": {
        "color": {
          "palette": [
            { "color": "#FFFFFF", "name": "Base",     "slug": "base" },
            { "color": "#111111", "name": "Contrast", "slug": "contrast" },
            { "color": "#FFEE58", "name": "Accent 1", "slug": "accent-1" }
          ]
        }
      },
      "styles": {
        "color": {
          "background": "var:preset|color|base",
          "text": "var:preset|color|contrast"
        }
      }
    }

    WordPress 读取这个 JSON 后,在 <head> 中生成如下 CSS:

    body {
        --wp--preset--color--base: #FFFFFF;
        --wp--preset--color--contrast: #111111;
        --wp--preset--color--accent-1: #FFEE58;
        /* ... */
    }

    所有 Block——按钮、引用、代码块、导航——都通过 var(--wp--preset--color--*) 引用这些变量。这是一个优雅的设计令牌系统。

    但问题来了:你无法在 JSON 里写「当系统偏好暗色时,把 Base 从白色换成 #1B1B1B」

    Style Variations 是 TT5 给出的折中方案:提供多套预设的调色板,让站长在后台手动选择一个。但这是「设计师的工具」,不是「访客的开关」。你选了 Evening 变体,全站就永远是暗色;你选了默认,全站就永远是亮色。

    这就是「最后一公里」问题:设计数据齐全,运行时切换缺失。

    WordPress Core 团队在 Gutenberg GitHub 上讨论过在 Global Styles 中原生支持 prefers-color-scheme 的可能性,但截至 2025 年底的 WordPress 6.7,这个功能仍未落地。原因不难理解——在一个驱动着全球 43% 网站的系统中,要确保数百万第三方插件和自定义 Block 的 CSS 兼容性,任何全局性的颜色翻转都是高风险操作。


    二、四套暗色调色板:官方已经回答了「什么颜色」

    在动手写代码之前,我仔细研读了 TT5 的 8 个 Style Variations。它们分为两个阵营:

    变体Base 色阵营设计气质
    Default#FFFFFF☀️ 亮纯净、中性
    Noon#F8F7F5☀️ 亮暖白、纸感
    Dusk#E2E2E2☀️ 亮浅灰、柔和
    Afternoon#DAE7BD☀️ 亮草绿、自然
    Morning#DFDCD7☀️ 亮米灰、晨光
    Evening#1B1B1B🌙 暗近黑、标准暗色
    Twilight#131313🌙 暗纯黑、高对比
    Sunrise#330616🌙 暗酒红、戏剧性
    Midnight#4433A6🌙 暗深紫、赛博朋克

    这不是随意的配色。每套暗色变体都完整定义了 8 个颜色 slug(base、contrast、accent-1 至 accent-6),并且针对按钮、引用、代码块等 Block 做了专门的样式调整。例如 Evening 变体将 accent-4 设为 #CBCBCB(柔和灰),用于正文文字,而非直接使用 contrast#F0F0F0),以减轻长时间暗色阅读的视觉疲劳。

    Midnight 变体更是大胆——以 #4433A6(深紫)为背景、#79F3B1(荧光绿)为前景,同时定义了专属的 duotone 滤镜,连图片都会统一为紫绿双色调。

    这些不是 AI 能随便生成的配色。这是设计师花时间打磨的系统级方案。

    所以真正的最佳实践不是自己发明暗色值,而是直接复用官方已经设计好的调色板


    三、第一性原理:只需覆盖 8 个 CSS 变量

    当你理解了 TT5 的颜色架构,解决方案就变得极其清晰:

    所有 Block 都通过 var(--wp--preset--color--*) 引用颜色 → 我只需要在暗色模式下重新定义这些变量的值 → 全站自动适配。

    不需要遍历每个 Block 写专门的暗色 CSS。不需要 filter: invert()(这是一种看似聪明实则灾难性的做法——它会反转图片、破坏渐变、摧毁 color-mix() 函数,并且完全无视设计师精心选择的暗色配色)。不需要触碰 theme.json

    核心 CSS 只有这些:

    html.tt5-dark-mode body {
        --wp--preset--color--base:     #1B1B1B;  /* Evening 变体 */
        --wp--preset--color--contrast: #F0F0F0;
        --wp--preset--color--accent-1: #786D0A;
        --wp--preset--color--accent-2: #442369;
        --wp--preset--color--accent-3: #D1D0EA;
        --wp--preset--color--accent-4: #CBCBCB;
        --wp--preset--color--accent-5: #353535;
        --wp--preset--color--accent-6: rgba(255, 255, 255, 0.2);
    }

    html.tt5-dark-mode body 的特异性高于 WordPress 生成的 body 选择器,所以变量会被覆盖。而因为所有 Block 样式都引用 var(),覆盖会自动级联到按钮、导航、搜索框、引用块、分隔线……一切。

    加上 color-scheme: dark 声明,连浏览器原生的表单元素和滚动条都会自动切换到深色风格:

    html.tt5-dark-mode {
        color-scheme: dark;
    }

    8 个变量 + 1 行 color-scheme = 全站暗色模式。

    这就是 CSS 自定义属性作为设计令牌的威力。TT5 的架构师们或许没有预见到暗色模式插件的出现,但他们选择用变量而非硬编码来构建颜色系统,这个决策本身就为后来者留出了精确的接缝。


    四、工程细节:魔鬼在「何时应用」

    CSS 变量覆盖只解决了「怎么切」,但一个生产级方案还需要回答三个问题:

    问题 1:如何在首次绘制之前应用偏好?(FOUC 防护)

    如果暗色模式的 class 由 JavaScript 在 DOMContentLoaded 后添加,用户会先看到一瞬间的白色页面再跳转到暗色——这就是 FOUC(Flash of Unstyled Content),体验极差。

    解法是在 <head> 中内联一段极小的同步脚本(约 400 字节),在浏览器开始绘制之前就读取 Cookie 并设置 class:

    (function(){
      var pref = (document.cookie.match(/tt5dm_pref=([^;]*)/) || [])[1] || 'auto';
      var dark = pref === 'auto'
        ? matchMedia('(prefers-color-scheme:dark)').matches
        : pref === 'dark';
      document.documentElement.classList.add(dark ? 'tt5-dark-mode' : 'tt5-light-mode');
    })();

    这段脚本是同步的、阻塞的——但因为只有 400 字节,对性能的影响可以忽略。它换来的是零闪烁的视觉体验。

    问题 2:如果站长已经选了暗色 Style Variation 怎么办?

    一个站长可能在后台选择了 Evening 作为默认样式。如果插件仍然「检测到系统暗色 → 应用暗色覆盖」,等于暗上加暗,毫无意义。

    解法是自动检测当前激活调色板的亮度

    function tt5dm_is_base_dark() {
        $palette = wp_get_global_settings( array( 'color', 'palette', 'theme' ) );
        // 找到 slug 为 'base' 的颜色,计算其 WCAG 相对亮度
        // 亮度 < 0.4 → 当前主题为暗色 → 插件反转逻辑,提供亮色切换
    }

    wp_get_global_settings() 是 WordPress 的 Global Styles API,它返回的是合并后的设置——包括 theme.json 的默认值和站长通过站点编辑器选择的 Style Variation。所以如果站长选了 Evening,这个函数会返回 Evening 的 #1B1B1B 作为 base 色。

    通过 WCAG 2.1 标准的相对亮度公式(sRGB → 线性 RGB → 加权求和),我们可以可靠地判断这个颜色是「亮」还是「暗」,然后自动决定插件应该提供暗色覆盖还是亮色覆盖。

    没有硬编码的颜色列表,没有「如果是 Evening 就怎样」的 if-else。纯粹基于色彩科学做判断。 这意味着即使未来 TT5 新增了第 9 个 Style Variation,插件也能正确工作。

    问题 3:为什么用 Cookie 而不是 localStorage?

    WordPress 前端环境多样——有些站点使用 CDN 缓存、有些运行在严格的 CSP 策略下、有些 iframe 嵌入场景会禁用 Storage API。Cookie 是最可靠的跨环境持久化方案,并且它在 <head> 内联脚本中可以同步读取(document.cookie),而 localStorage 虽然也是同步的,但在某些受限环境中会抛出异常。

    一个功能性 Cookie(tt5dm_pref),存储 auto/dark/light 三个值之一,有效期 365 天。就这么简单。


    五、关于 Vibe Coding:它不是「让 AI 写代码」那么简单

    现在让我们聊聊 Vibe Coding。

    网上流传的 Vibe Coding 叙事往往是这样的:「告诉 AI 你想要什么氛围,它就帮你生成代码。」然后给出一段 filter: invert(100%) hue-rotate(180deg) 作为「极简暗色模式方案」。

    这恰恰是 Vibe Coding 的反面。

    filter: invert() 看起来很 vibe——一行代码、全站翻转、很有「黑客感」。但它:

    • 会反转所有图片和视频(然后你需要再反转一次来「修复」);
    • 会破坏 color-mix()linear-gradient() 等现代 CSS 函数;
    • 完全无视 TT5 设计师精心选择的暗色配色(Evening 的 accent-1 是 #786D0A 暗金色,不是亮黄色 #FFEE58 的色相反转);
    • 在嵌套 Block(Group 里套 Group)中会产生双重反转;
    • color-scheme 原生表单适配毫无帮助。

    真正的 Vibe 不是「我不想理解系统所以让 AI 随便搞」,而是「我理解系统的设计意图,然后在正确的接缝处施加最小的干预来实现最大的效果」。

    在 TT5 的语境下,这个「正确的接缝」就是 CSS 自定义属性层。theme.json 定义了它们,所有 Block 消费它们,我们只需要在运行时有条件地覆盖它们。这不是破坏系统,而是顺着系统的纹理工作。

    AI 在这个过程中的真正价值不在于生成那 8 行 CSS 变量覆盖(这任何中级开发者都能写),而在于:

    1. 快速逆向工程:在几分钟内遍历 TT5 的全部 100+ 文件,确认「没有 prefers-color-scheme」「没有 JavaScript」「functions.php 只有 167 行」——这种全局性的代码考古,AI 做得比人快一个数量级。
    2. 跨层知识连接:从 theme.json 的 JSON schema 到 wp_get_global_settings() 的 PHP API,再到 WCAG 亮度算法的数学公式,再到浏览器 color-scheme 属性的渲染行为——这种跨越 4 个技术层的知识整合,是 AI 辅助编程的甜蜜点。
    3. 架构决策的快速验证:「用 CSS 媒体查询还是 class?」「用 localStorage 还是 Cookie?」「亮度阈值设 0.4 还是 0.5?」——每个决策都有 trade-off,AI 可以在几秒内给出各方案的优劣对比,帮助人类更快地做出判断。

    这才是 Vibe Coding 的正确打开方式:人类定义意图和约束,AI 加速探索和实现,最终产出的是符合系统设计哲学的精确干预,而不是绕过系统的粗暴 hack。


    六、小结:自定义的前提是理解

    「自定义即原生」——这句话只在你理解了什么是「原生」之后才成立。

    TT5 没有暗色模式开关,不是因为 WordPress 团队不想做,而是因为 theme.json 的静态本质和 Global Styles 的架构约束使得运行时颜色切换无法在现有框架内优雅实现。

    但 TT5 的设计师们做了一件更重要的事:他们用 CSS 自定义属性构建了一个完全令牌化的颜色系统,并提供了 4 套经过专业设计的暗色调色板。 这不是半成品,这是深思熟虑的架构决策——把「何时切换」的问题留给更擅长处理运行时逻辑的层(JavaScript/PHP),而在自己的职责范围内(设计数据)做到了完美。

    我们要做的,不是重新发明颜色,而是在正确的层(CSS 特异性覆盖 + 微量 JS)补上最后一块拼图。

    当你看到一个「缺失」的功能时,先问三个问题:

    1. 系统在这里为什么没做?(理解约束)
    2. 系统在这里留下了什么接口?(发现接缝)
    3. 最小的干预是什么?(精确施工)

    这三个问题的答案,比任何 AI 生成的代码都更有价值。而 AI 最擅长帮你的,恰恰是更快地找到这三个答案。

    注:本文涉及的所有技术结论均基于 WordPress Twenty Twenty-Five v1.4 源码分析。文中提及的 TT5 Dark Mode 插件由 BraveDAO 开发,完整实现了上述方案,总计约 35KB(PHP + CSS + JS),零外部依赖。

    Brave 回复 6 days ago 1 成員 · 0 回复
  • 0 回复

歡迎留言回复交流。

Log in to reply.

讨论開始
00 回复 2018 年 6 月
現在