Ghost 主题暗色模式实现指南
在 Ember 主题中引入暗色模式的完整踩坑过程,总结了两种主流方案及其各自的优劣。
背景
Ghost 官方默认主题 Source 内置了一套对比度自适应机制,但对于使用 @tryghost/shared-theme-assets 构建的主题(如 Ember),直接套用 Source 的方案会遇到一系列问题。
方案一:全量暗色模式(Class Toggle)
思路
新增一个 CSS class(如 html.dark-mode),在其中翻转所有颜色变量,并通过 JavaScript 切换按钮让用户在 Light/Dark/Auto 之间手动选择。
实现
html.dark-mode {
--color-white: #1e1e2e;
--color-lighter-gray: #262637;
--color-light-gray: #2e2e42;
--color-mid-gray: #3e3e56;
--color-dark-gray: #888;
--color-darker-gray: #e0e0e0;
--color-primary-text: #e0e0e0;
--color-secondary-text: #888;
--color-black: #0d0d1a;
color-scheme: dark;
}然后为每个受影响的组件写暗色覆盖规则。方案需要大约 460 行 CSS。
踩坑
坑 1:Ghost Portal 按钮显示白色背景
Ghost Portal 的浮动按钮和会员模态框渲染在 <iframe> 内,主题 CSS 无法直接样式化。Portal 通过检测其容器 #ghost-portal-root 的计算背景色来判断使用浅色还是深色 UI。
#ghost-portal-root 是一个 position: fixed 的 <div>,默认背景透明。即使你设置了 body { background: dark },Portal 读到的是 transparent,无法判定为深色模式,于是始终渲染浅色 UI。
尝试通过 color-scheme: dark 告知 Portal 使用深色模式:
html.dark-mode #ghost-portal-root,
html.dark-mode #ghost-portal-root iframe {
color-scheme: dark;
}这又引入了新问题——color-scheme: dark 会让浏览器给 iframe 内部文档设默认深色背景,破坏了透明性,Portal UI 反而出现了意外的深色块。
坑 2:页面漂移
Ghost Portal 打开搜索或会员模态框时,会给 <body> 设 overflow: hidden 并添加 margin-right 来补偿滚动条宽度。即使通过 CSS 抵消了 body 的 margin,Portal 还会给 iframe 自身添加 margin-right: calc(15px)(15px 是滚动条宽度),导致 Portal 浮动按钮向左偏移。
修复需要同时抵消三处:
body[style*="overflow: hidden"] {
margin-right: 0 !important;
padding-right: 0 !important;
}
body[style*="overflow: hidden"] #ghost-portal-root,
body[style*="overflow: hidden"] #ghost-portal-root iframe {
margin-right: 0 !important;
}
html {
scrollbar-gutter: stable;
}方案二:Source 风格对比度自适应
思路
Source 主题采用完全不同的策略:
- 不设开关——站长在 Ghost Admin 中选择背景色
- YIQ 算法计算背景色的亮度,自动判定文字应该是深色还是浅色
- 只翻转 4-5 个关键变量——文字、边框、次要元素,不碰 --color-white 和 --color-black
- 不需要组件级覆盖——变量翻转自动级联到所有引用处
CSS 变量设计
Source 的 :root 变量设计中,核心技巧是 --color-primary-text 引用 --color-darker-gray:
:root {
--color-primary-text: var(--color-darker-gray); /* 间接引用! */
--color-darker-gray: #15171a;
--color-white: #fff;
}暗色模式下只需翻转被引用的变量:
:root.has-light-text {
--color-darker-gray: #fff; /* 文字变白 */
--color-secondary-text: rgb(255 255 255 / 0.64); /* 次要文字 */
--color-lighter-gray: rgb(255 255 255 / 0.1); /* 微妙背景 */
--color-light-gray: rgb(255 255 255 / 0.15); /* 边框 */
--color-mid-gray: rgb(255 255 255 / 0.3);
--color-dark-gray: rgb(255 255 255 / 0.55);
}只需约 60 行 CSS。
模板集成
default.hbs 的 <head> 中设置背景色并运行 YIQ 脚本:
<style>
:root {
--background-color: {{@custom.site_background_color}}
}
</style>
<script>
var accentColor = getComputedStyle(document.documentElement)
.getPropertyValue('--background-color');
accentColor = accentColor.trim().slice(1);
// 3位hex扩展为6位
if (accentColor.length === 3) {
accentColor = accentColor[0] + accentColor[0]
+ accentColor[1] + accentColor[1]
+ accentColor[2] + accentColor[2];
}
var r = parseInt(accentColor.substr(0, 2), 16);
var g = parseInt(accentColor.substr(2, 2), 16);
var b = parseInt(accentColor.substr(4, 2), 16);
var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
var textColor = (yiq >= 128) ? 'dark' : 'light';
document.documentElement.className = 'has-' + textColor + '-text';
</script>package.json 使用 color 类型设置:
"site_background_color": {
"type": "color",
"default": "#ffffff"
}为什么 Source 方案更适配 Portal
Portal 的容器 #ghost-portal-root 虽然背景透明,但它会向上遍历 DOM 树查找第一个非透明背景。Source 方案中,body 的背景通过 --background-color 直接使用了站长选择的颜色。当站长选择 #1a1a2e 时,body 的实际计算背景就是深色,Portal 能够正确检测到——不需要任何额外的 Portal CSS 规则。
方案二的例外情况
纯粹的变量翻转无法处理同一变量语义相反的场景。例如:
| 问题 | 原因 | 修复 |
|---|---|---|
| 按钮 | bg: var(--color-darker-gray) 在暗色模式变成白色背景 |
强制浅色背景+深色文字 |
| 页脚 | 同上,深色页脚背景变白 | 强制纯黑背景 |
| 卡片 | 背景 var(--color-white) 不变但文字翻白 |
卡片背景切为深色 |
这些例外只需 5 条规则,远少于方案一的 40+ 条。
总结对比
| 维度 | 方案一 (Full Dark Mode) | 方案二 (Source 风格) |
|---|---|---|
| CSS 行数 | ~460 | ~60 |
| Portal 兼容 | 需要显式 hack | 自然兼容 |
| 用户控制 | 有切换按钮 | 站长设定背景色 |
| OS 检测 | 支持 prefers-color-scheme |
不检测(站长驱动) |
| 维护成本 | 高(每个新组件可能需要覆盖) | 低(变量翻转自动级联) |
| 设计哲学 | 全局翻转为暗色 UI | 背景可变,内容区保持亮色 |
最终建议
对于基于 @tryghost/shared-theme-assets 构建的主题,强烈推荐方案二。原因:
- Ghost Portal、Comments 等嵌入式 iframe 能自然适配,无需 hack
- 变量翻转的设计使得主题 CSS 保持简洁,长期维护成本低
- 与 Ghost 生态的设计哲学一致——站长控制外观,而非访客
- 正确的 body 背景色让 Portal 的自动检测机制正常运行
评论 ()