Skip to content

暗色模式深度定制

VitePress 内置暗色模式支持,本文介绍如何深度定制暗色模式的视觉效果,包括颜色体系、对比度优化、图片适配、组件适配和无障碍支持。

暗色模式基础

启用和配置

typescript
// .vitepress/config.mts
export default defineConfig({
  // true: 支持亮/暗切换(默认)
  // 'dark': 默认暗色,可切换
  // 'force-dark': 强制暗色
  // 'force-auto': 强制跟随系统
  // false: 禁用暗色模式
  appearance: true
})

工作原理

VitePress 的暗色模式通过以下机制实现:

  1. <html> 标签上添加 class="dark"
  2. 使用 CSS 变量切换颜色方案
  3. 通过 localStorage 持久化用户偏好
html
<!-- 亮色模式 -->
<html lang="zh-CN">

<!-- 暗色模式 -->
<html lang="zh-CN" class="dark">

对比度与无障碍

WCAG 对比度标准

暗色模式不仅是视觉偏好,更关乎可访问性。WCAG 2.1 定义了两个级别的对比度要求:

级别正文文本(< 18px)大文本(≥ 18px 或 14px 加粗)UI 组件
AA≥ 4.5:1≥ 3:1≥ 3:1
AAA≥ 7:1≥ 4.5:1≥ 4.5:1

常见误区

  • 不是所有深色背景 + 白色文字就能满足对比度要求。rgba(255, 255, 255, 0.6)#1b1b1f 背景上对比度仅约 4.0:1,不满足 AA 标准
  • 品牌色在亮色模式好看,但在深色背景上可能对比度不足
  • 次要文本(--vp-c-text-2--vp-c-text-3)是最容易出问题的区域

对比度检测工具

工具用途地址
WebAIM Contrast Checker在线对比度计算https://webaim.org/resources/contrastchecker/
Chrome DevTools实时对比度检查Elements → Color 预览旁的对比度比值
axe DevTools自动化无障碍审计Chrome 扩展
Polypane多视口 + 对比度检查https://polypane.app/
Colour Contrast Analyser桌面应用(支持色盲模拟)https://www.tpgi.com/color-contrast-checker/

对比度自查流程

bash
# 1. 使用 Chrome DevTools 检查
# 打开页面 → F12 → 选择文本元素 → 查看 Color 属性旁的对比度值

# 2. 自动化检测(推荐)
npx axe-core --include .dark

手动检查关键元素对比度:

css
/* 检查清单:确保以下组合满足 WCAG AA */
:root {
  /* 亮色:正文 vs 背景 */
  --check-1: rgba(60, 60, 67) on #ffffff;       /* ~9.5:1 ✓ */

  /* 暗色:正文 vs 背景 */
  --check-2: rgba(255, 255, 245, 0.86) on #1b1b1f;  /* ~10:1 ✓ */

  /* 暗色:品牌色 vs 背景 */
  --check-3: #a5b4fc on #0f0f10;                /* ~8.5:1 ✓ */

  /* 暗色:次要文本 vs 背景(最易出问题) */
  --check-4: rgba(235, 235, 245, 0.6) on #1b1b1f; /* ~4.7:1 ✓ 刚好 AA */
}

色觉无障碍(色盲适配)

约 8% 的男性和 0.5% 的女性存在色觉差异。仅靠颜色传达信息时需特别处理:

css
/* ❌ 错误:仅用颜色区分状态 */
.status-success { color: #10b981; }
.status-error { color: #ef4444; }

/* ✅ 正确:颜色 + 图标/文字 */
.status-success { color: #10b981; }
.status-success::before { content: '✓ '; }
.status-error { color: #ef4444; }
.status-error::before { content: '✗ '; }

暗色模式下的色觉适配要点

场景问题解决方案
链接与正文蓝色链接对色弱用户不明显添加下划线或加粗
成功/错误红/绿色盲无法区分使用图标 + 文字标签
图表颜色多色系难以分辨使用不同图案/形状辅助
悬停状态颜色变化不明显增加 transformborder 变化

CSS 变量体系

品牌色变量

暗色模式下品牌色需要调亮,确保在深色背景上有足够对比度:

css
:root {
  /* 亮色品牌色 */
  --vp-c-brand: #646cff;
  --vp-c-brand-light: #747bff;
  --vp-c-brand-lighter: #9499ff;
  --vp-c-brand-dark: #535bf2;
  --vp-c-brand-darker: #454ce1;
}

.dark {
  /* 暗色品牌色 — 通常比亮色版本更亮 */
  --vp-c-brand: #747bff;
  --vp-c-brand-light: #9499ff;
  --vp-c-brand-lighter: #b0b4ff;
  --vp-c-brand-dark: #6366f1;
  --vp-c-brand-darker: #535bf2;
}

背景和前景色

css
:root {
  --vp-c-bg: #ffffff;              /* 页面背景 */
  --vp-c-bg-soft: #f6f6f7;        /* 柔和背景 */
  --vp-c-bg-mute: #f1f1f2;        /* 静音背景 */
  --vp-c-bg-alt: #ffffff;          /* 交替背景 */

  --vp-c-text-1: rgba(60, 60, 67); /* 主文本 */
  --vp-c-text-2: rgba(60, 60, 67, 0.78); /* 次文本 */
  --vp-c-text-3: rgba(60, 60, 67, 0.56); /* 辅助文本 */
}

.dark {
  --vp-c-bg: #1b1b1f;
  --vp-c-bg-soft: #1b1b1f;
  --vp-c-bg-mute: #252529;
  --vp-c-bg-alt: #1b1b1f;

  --vp-c-text-1: rgba(255, 255, 245, 0.86);
  --vp-c-text-2: rgba(235, 235, 245, 0.6);
  --vp-c-text-3: rgba(235, 235, 245, 0.38);
}

边框和分割线

css
:root {
  --vp-c-divider: rgba(60, 60, 67, 0.12);
  --vp-c-border: rgba(60, 60, 67, 0.29);
}

.dark {
  --vp-c-divider: rgba(84, 84, 88, 0.48);
  --vp-c-border: rgba(84, 84, 88, 0.65);
}

组件特定变量

css
.dark {
  /* 导航栏 */
  --vp-nav-bg-color: rgba(27, 27, 31, 0.8);
  
  /* 侧边栏 */
  --vp-sidebar-bg-color: #1b1b1f;
  
  /* 代码块 */
  --vp-code-bg-color: #252529;
  --vp-code-color: #c9d1d9;
  
  /* 自定义容器 */
  --vp-custom-block-tip-bg: rgba(100, 108, 255, 0.08);
  --vp-custom-block-warning-bg: rgba(255, 186, 0, 0.08);
  --vp-custom-block-danger-bg: rgba(255, 75, 75, 0.08);
  
  /* 搜索框 */
  --vp-local-search-bg: #252529;
}

图片暗色适配

方式一:CSS Filter 反转

适合简单图标和线条图:

css
.dark img:not(.no-invert) {
  filter: invert(1) hue-rotate(180deg);
}

/* 排除不需要反转的图片 */
.dark img.logo,
.dark img.photo {
  filter: none;
}

注意

照片和复杂图片不适合用 filter: invert(),会导致色彩失真。

方式二:<picture> 元素

为暗色模式提供不同的图片版本:

html
<picture>
  <source srcset="/images/dark/diagram.png" media="(prefers-color-scheme: dark)">
  <img src="/images/light/diagram.png" alt="架构图">
</picture>

方式三:Vue 组件动态切换

vue
<script setup lang="ts">
import { useData } from 'vitepress'
import lightImg from '/images/light/chart.png'
import darkImg from '/images/dark/chart.png'

const { isDark } = useData()
</script>

<template>
  <img :src="isDark ? darkImg : lightImg" alt="图表" />
</template>

方式四:CSS 变量控制

css
:root {
  --hero-image: url('/images/hero-light.png');
}

.dark {
  --hero-image: url('/images/hero-dark.png');
}

.hero-image {
  background-image: var(--hero-image);
}

组件暗色适配

自定义组件适配暗色模式

vue
<script setup lang="ts">
import { useData } from 'vitepress'

const { isDark } = useData()
</script>

<template>
  <div class="custom-card" :class="{ 'custom-card--dark': isDark }">
    <h3>卡片标题</h3>
    <p>卡片内容</p>
  </div>
</template>

<style scoped>
.custom-card {
  padding: 16px;
  border-radius: 8px;
  background-color: #f5f5f5;
  border: 1px solid #e0e0e0;
  color: #333;
}

.custom-card--dark {
  background-color: #2a2a2e;
  border-color: #444;
  color: #e0e0e0;
}
</style>

推荐:使用 CSS 变量

vue
<template>
  <div class="custom-card">
    <h3>卡片标题</h3>
    <p>卡片内容</p>
  </div>
</template>

<style scoped>
.custom-card {
  padding: 16px;
  border-radius: 8px;
  /* 使用 CSS 变量,自动适配暗色模式 */
  background-color: var(--vp-c-bg-soft);
  border: 1px solid var(--vp-c-divider);
  color: var(--vp-c-text-1);
}
</style>

代码块暗色主题

配置语法高亮主题

typescript
export default defineConfig({
  markdown: {
    theme: {
      light: 'vitesse-light',
      dark: 'vitesse-dark'
    }
  }
})

可用主题

主题风格适用场景
vitesse-light / vitesse-dark柔和暖色调通用推荐
github-light / github-darkGitHub 风格技术文档
material-theme-lighter / material-theme-palenightMaterial 风格现代感
nord冷色调简约风格
solarized-light / solarized-dark护眼色调长文阅读

自定义代码高亮颜色

css
.dark {
  /* 关键字 */
  --shiki-token-keyword: #c678dd;
  /* 字符串 */
  --shiki-token-string: #98c379;
  /* 函数 */
  --shiki-token-function: #61afef;
  /* 注释 */
  --shiki-token-comment: #5c6370;
  /* 数字 */
  --shiki-token-number: #d19a66;
}

过渡动画

添加模式切换过渡

css
/* 平滑的颜色过渡 */
:root {
  color-scheme: light;
}

.dark {
  color-scheme: dark;
}

html {
  transition: color 0.3s, background-color 0.3s;
}

禁用特定元素的过渡

css
img,
video,
iframe {
  transition: none !important;
}

常见问题

闪烁问题(FOUC)

暗色模式页面首次加载时可能出现白色闪烁。解决方法:

html
<!-- 在 <head> 中添加内联脚本 -->
<script>
  if (localStorage.getItem('vitepress-theme-appearance') === 'dark' ||
      (!localStorage.getItem('vitepress-theme-appearance') && 
       window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark')
  }
</script>

VitePress 默认已处理此问题,但自定义主题可能需要手动添加。

自定义组件未适配暗色模式

确保使用 CSS 变量而非硬编码颜色:

css
/* ❌ 错误:硬编码颜色 */
.my-component {
  background: #ffffff;
  color: #333333;
}

/* ✅ 正确:使用 CSS 变量 */
.my-component {
  background: var(--vp-c-bg);
  color: var(--vp-c-text-1);
}

Canvas / SVG 暗色适配

css
/* SVG 图标适配 */
.dark svg {
  fill: var(--vp-c-text-1);
}

/* Canvas 需要在 JS 中动态处理 */
vue
<script setup lang="ts">
import { useData } from 'vitepress'
import { ref, watch } from 'vue'

const { isDark } = useData()

watch(isDark, (dark) => {
  const canvas = document.querySelector('canvas')
  if (canvas) {
    // 重新绘制 Canvas
    drawCanvas(dark)
  }
})
</script>

检查清单

暗色模式适配完成后的验证清单:

  • [ ] 所有文本在暗色背景上有足够对比度(WCAG AA ≥ 4.5:1)
  • [ ] 品牌色在暗色模式下清晰可见(对比度 ≥ 4.5:1)
  • [ ] 代码块高亮主题已配置
  • [ ] 图片在暗色模式下正确显示
  • [ ] 自定义组件使用 CSS 变量(无硬编码颜色)
  • [ ] 无页面闪烁(FOUC)
  • [ ] 模式切换过渡平滑
  • [ ] 搜索框在暗色模式下可用
  • [ ] 表格边框在暗色模式下可见
  • [ ] 自定义容器样式正确
  • [ ] 浏览器地址栏 theme-color 随模式切换
  • [ ] 色觉无障碍:关键信息不仅依赖颜色传达
  • [ ] PWA manifest 支持暗色模式主题色

动态 theme-color

浏览器地址栏和 PWA 的主题色应随暗色模式切换,提升整体一致性。

浏览器地址栏主题色

VitePress 默认只设置一个固定的 theme-color,需要动态切换:

typescript
// .vitepress/config.mts
export default defineConfig({
  head: [
    // 仅设置浅色默认值,动态切换在 theme/index.ts 中处理
    ['meta', { name: 'theme-color', content: '#6366f1' }]
  ]
})
typescript
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
import { watch } from 'vue'

export default {
  extends: DefaultTheme,
  setup() {
    const { isDark } = useData()

    // 动态切换浏览器地址栏主题色
    watch(isDark, (dark) => {
      const meta = document.querySelector('meta[name="theme-color"]')
      if (meta) {
        meta.setAttribute('content', dark ? '#0f0f10' : '#6366f1')
      }
    }, { immediate: true })
  }
}

PWA 主题色适配

PWA 的 theme_colorbackground_color 在 manifest 中是静态的,需要通过动态生成或双 manifest 方案适配:

typescript
// 方案一:动态修改 manifest(推荐)
if (typeof window !== 'undefined') {
  const link = document.querySelector('link[rel="manifest"]')
  if (link) {
    const { isDark } = useData()
    watch(isDark, (dark) => {
      link.href = dark ? '/manifest-dark.webmanifest' : '/manifest.webmanifest'
    })
  }
}
jsonc
// public/manifest.webmanifest(浅色)
{
  "theme_color": "#6366f1",
  "background_color": "#ffffff"
}

// public/manifest-dark.webmanifest(深色)
{
  "theme_color": "#0f0f10",
  "background_color": "#0f0f10"
}

常见对比度问题修复

问题一:硬编码颜色对比度不足

css
/* ❌ 问题:硬编码颜色在暗色模式下对比度不足 */
.badge-hot {
  background: #fee2e2;
  color: #ef4444;
}

/* ✅ 修复:暗色模式使用更亮的颜色 */
.badge-hot {
  background: #fee2e2;
  color: #ef4444;
}

.dark .badge-hot {
  background: rgba(239, 68, 68, 0.2);
  color: #fca5a5; /* 更亮的红色,对比度更高 */
}

问题二:次要文本对比度不足

css
/* ❌ 问题:透明度过低 */
.dark {
  --vp-c-text-2: rgba(255, 255, 255, 0.5); /* 对比度 ~3.5:1,不满足 AA */
}

/* ✅ 修复:提高透明度 */
.dark {
  --vp-c-text-2: rgba(255, 255, 255, 0.7); /* 对比度 ~5.5:1,满足 AA ✓ */
}

问题三:代码块内嵌代码对比度

css
/* ❌ 问题:暗色模式内嵌代码文本不够亮 */
.dark .vp-doc code {
  color: rgba(255, 255, 255, 0.7);
}

/* ✅ 修复:确保对比度 */
.dark .vp-doc code {
  color: rgba(255, 255, 255, 0.9); /* 对比度更高 ✓ */
}

问题四:占位符文本对比度

css
/* ❌ 占位符颜色太暗,暗色模式看不清 */
input::placeholder {
  color: rgba(60, 60, 67, 0.4);
}

/* ✅ 修复:暗色模式提升占位符亮度 */
.dark input::placeholder {
  color: rgba(255, 255, 255, 0.5);
}

高对比度模式

系统级高对比度支持

操作系统提供高对比度模式,VitePress 项目已内置支持:

css
/* variables.css 中的高对比度覆盖 */
@media (prefers-contrast: more) {
  .dark {
    --vp-c-text-1: #ffffff;
    --vp-c-text-2: rgba(255, 255, 255, 0.85);
    --vp-c-text-3: rgba(255, 255, 255, 0.7);
    --vp-c-brand-1: #c7d2fe;
    --vp-c-divider: rgba(255, 255, 255, 0.2);
  }
}
css
/* accessibility.css 中的组件级高对比度 */
@media (prefers-contrast: high) {
  .dark {
    --vp-c-divider: rgba(255, 255, 255, 0.3);
  }

  /* 链接下划线增强 */
  .vp-doc a {
    text-decoration-thickness: 2px;
  }

  /* 表格边框加粗 */
  .vp-doc table,
  .vp-doc th,
  .vp-doc td {
    border-width: 2px;
  }
}

强制暗色 + 高对比度

force-dark 配置配合系统高对比度模式,可以同时满足暗色偏好和高对比度需求:

typescript
export default defineConfig({
  // 强制暗色,跟随系统高对比度
  appearance: 'force-dark'
})

深色模式设计原则

颜色层次

深色模式的颜色不应是浅色的简单反转,而是遵循以下原则:

原则说明示例
降低亮度而非反转色相保持色相一致,仅调整明度亮色 #6366f1 → 暗色 #a5b4fc(同色相,更亮)
分层使用透明度rgba 的 alpha 值建立层次文本 1.0 > 0.7 > 0.5
背景越深,边框越亮增强边界可辨识度分割线 rgba(255,255,255,0.1)
语义色需要提亮深色背景上颜色感知变暗success #10b981#34d399
避免纯黑背景#000000 反差过强,眼睛疲劳使用 #0f0f10#1b1b1f

间距与层次

深色模式下,视觉层次更依赖间距和边框:

css
/* 暗色模式下增大卡片间距 */
.dark .card + .card {
  margin-top: 20px; /* 比浅色多 4px */
}

/* 暗色模式下增强边框可见性 */
.dark .card {
  border-width: 1px;
  border-color: rgba(255, 255, 255, 0.12);
}

相关链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献