Ghost 主题暗色模式实现指南

在 Ember 主题中引入暗色模式的完整踩坑过程,总结了两种主流方案及其各自的优劣。

Ghost 主题暗色模式实现指南

背景

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 主题采用完全不同的策略:

  1. 不设开关——站长在 Ghost Admin 中选择背景色
  2. YIQ 算法计算背景色的亮度,自动判定文字应该是深色还是浅色
  3. 只翻转 4-5 个关键变量——文字、边框、次要元素,不碰 --color-white--color-black
  4. 不需要组件级覆盖——变量翻转自动级联到所有引用处

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 构建的主题,强烈推荐方案二。原因:

  1. Ghost Portal、Comments 等嵌入式 iframe 能自然适配,无需 hack
  2. 变量翻转的设计使得主题 CSS 保持简洁,长期维护成本低
  3. 与 Ghost 生态的设计哲学一致——站长控制外观,而非访客
  4. 正确的 body 背景色让 Portal 的自动检测机制正常运行
转发至

微信扫一扫分享

WeChat QR Code