> ## Content Index
> Fetch the complete content index at: https://yinguobing.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ghost 主题暗色模式实现指南
- URL: https://yinguobing.com/blog/ghost-theme-dark-mode-guide/
- Published: 2026-06-01T14:41:07.000Z
- Updated: 2026-06-01T15:12:13.000Z
- Description: 在 Ember 主题中引入暗色模式的完整踩坑过程，总结了两种主流方案及其各自的优劣。
- Author: 小黑
- Tags: Ghost, 前端, 开源

## 背景

Ghost 官方默认主题 [Source](https://github.com/TryGhost/Source?ref=yinguobing.com) 内置了一套对比度自适应机制，但对于使用 @tryghost/shared-theme-assets 构建的主题（如 Ember），直接套用 Source 的方案会遇到一系列问题。

## 方案一：全量暗色模式（Class Toggle）

### 思路

新增一个 CSS class（如 html.dark-mode），在其中翻转所有颜色变量，并通过 JavaScript 切换按钮让用户在 Light/Dark/Auto 之间手动选择。

### 实现

```css
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 使用深色模式：

```css
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 浮动按钮向左偏移。

修复需要同时抵消三处：

```css
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：

```css
:root {
    --color-primary-text: var(--color-darker-gray);  /* 间接引用！ */
    --color-darker-gray: #15171a;
    --color-white: #fff;
}
```

暗色模式下只需翻转被引用的变量：

```css
: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 脚本：

```html
<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 类型设置：

```json
"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 的自动检测机制正常运行