TECH · 3 MIN READ · 1236 字

暗色不是反色:给博客做一套可切换的双主题

亮色主题不是把暗色倒过来。它需要重新定义背景、文字、边框、图片遮罩与页脚的关系,还要避免刷新时闪一下白。

亮色主题不是把暗色倒过来。它需要重新定义背景、文字、边框、图片遮罩与页脚的关系,还要避免刷新时闪一下白。

这个博客默认暗色。亮色不是附属品,而是一套完整的第二皮肤。

为什么不直接反色

最省事的做法是给整个页面加 filter: invert(1),再把图片反回来。但这会带来几个问题:

  • 黄色会变成蓝色,品牌色被破坏。
  • 图片反色后颜色失真,需要逐个修正。
  • 阴影、透明遮罩、渐变会得到奇怪的结果。
  • 对比度无法按真实文字层级调整。

所以这里用的是 CSS 变量 + 局部覆盖,不是反色。

主题状态放在 <html> 上

<html lang="zh-CN" data-theme="dark">

默认暗色。切换时只改根元素的 data-theme:

document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
localStorage.setItem("lanr-theme", theme);

color-scheme 会告诉浏览器表单控件、滚动条等使用哪种配色,避免亮色主题里出现深色输入框。

首屏不能闪

如果等 main.js 执行后再切主题,亮色用户会先看到一帧暗色。解决办法是在 <head> 里、CSS 之前放一个极小的同步脚本:

<script src="/assets/theme-init.js"></script>
<link rel="stylesheet" href="/assets/style.css">

theme-init.js 只做一件事:

let theme = "dark";
try {
  const stored = localStorage.getItem("lanr-theme");
  if (stored === "light" || stored === "dark") theme = stored;
} catch {}
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;

脚本不到 500 字节,代价是一次很小的阻塞请求;换来的是没有主题闪烁。

变量怎么分层

暗色和亮色共享同一组语义变量:

:root {
  --bg: #07060a;
  --ink: #f5f2ff;
  --muted: #a49cb9;
  --line: rgba(255, 255, 255, 0.1);
  --yellow: #ffe600;
  --purple: #a855f7;
}

html[data-theme="light"] {
  --bg: #f3eee3;
  --ink: #17121b;
  --muted: #665d70;
  --line: rgba(23, 18, 27, 0.13);
  --yellow: #a16207;
  --purple: #6d28d9;
}

这里有一个关键取舍:黄色分成两种角色。

  • --yellow 用于文字、下划线、图标等“黄色墨水”。亮色下用深琥珀 #a16207 保证可读性。
  • --yellow-bright 用于按钮、标签、跑马灯等“黄色色块”。两种主题都保持 #ffe600,因为色块上是黑字。

如果只保留一个黄色变量,亮色下要么文字看不清,要么按钮失去霓虹感。

页脚必须跟着主题走

最初的实现里,页脚被固定成深色,理由是“作为视觉锚点”。但切到亮色后,页面底部突然出现一块黑色,像另一个网站。

修正方式是移除页脚上的局部变量覆盖,让它继承当前主题:

html[data-theme="light"] .site-footer {
  background: var(--bg-soft);
  border-top-color: var(--line);
  color: var(--ink);
}

同时把页脚压缩成一行信息密度更高的区域,不再用巨大的描边字占据半屏。主题切换时,页脚背景、文字、边框、链接 hover 都跟着变。

这类问题不是“颜色没配好”,而是组件的主题边界没有定义清楚。页脚曾经是一个例外,现在它和正文一样,只是页面的一部分。

图片与遮罩也要分主题

首屏插画在两种主题下共用,但遮罩不同:

  • 暗色:深色渐变把左侧压暗,文字用白色。
  • 亮色:暖纸色渐变从左侧覆盖,文字用近黑色,图片保留更多颜色。
html[data-theme="light"] .hero__media::before {
  background:
    linear-gradient(90deg, var(--bg) 0%, rgba(243, 238, 227, 0.88) 18%, transparent 66%),
    linear-gradient(180deg, rgba(243, 238, 227, 0.38), transparent 30%);
}

如果只在文字上改颜色,图片仍然是暗的,亮色主题会显得脏。遮罩是主题的一部分。

哪些地方仍然保持深色

代码块是例外。正文里的 <pre> 在亮色主题下仍然是深色背景,因为代码的高亮和等宽字体在深底上更稳定。它被视为“嵌入的编辑器”,不是页面背景。

验收

主题功能最后用真实浏览器检查:

  • 暗色 → 亮色切换成功。
  • localStorage 写入 lanr-theme,刷新后保持。
  • <meta name="theme-color"> 从 #07060a 更新为 #f3eee3。
  • 0 控制台错误,0 失败请求。
  • 11 个公开路由与移动端都没有横向溢出。

主题不是多一套颜色,而是多一套阅读环境。