如何让 WordPress Twenty Twenty-Five 主题支持暗色模式
-
如何让 WordPress Twenty Twenty-Five 主题支持暗色模式
目录本文基于对 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 变量覆盖(这任何中级开发者都能写),而在于:
- 快速逆向工程:在几分钟内遍历 TT5 的全部 100+ 文件,确认「没有 prefers-color-scheme」「没有 JavaScript」「functions.php 只有 167 行」——这种全局性的代码考古,AI 做得比人快一个数量级。
- 跨层知识连接:从
theme.json的 JSON schema 到wp_get_global_settings()的 PHP API,再到 WCAG 亮度算法的数学公式,再到浏览器color-scheme属性的渲染行为——这种跨越 4 个技术层的知识整合,是 AI 辅助编程的甜蜜点。 - 架构决策的快速验证:「用 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)补上最后一块拼图。
当你看到一个「缺失」的功能时,先问三个问题:
- 系统在这里为什么没做?(理解约束)
- 系统在这里留下了什么接口?(发现接缝)
- 最小的干预是什么?(精确施工)
这三个问题的答案,比任何 AI 生成的代码都更有价值。而 AI 最擅长帮你的,恰恰是更快地找到这三个答案。
注:本文涉及的所有技术结论均基于 WordPress Twenty Twenty-Five v1.4 源码分析。文中提及的 TT5 Dark Mode 插件由 BraveDAO 开发,完整实现了上述方案,总计约 35KB(PHP + CSS + JS),零外部依赖。
歡迎留言回复交流。
Log in to reply.