暗色模式深度定制
VitePress 内置暗色模式支持,本文介绍如何深度定制暗色模式的视觉效果,包括颜色体系、对比度优化、图片适配、组件适配和无障碍支持。
暗色模式基础
启用和配置
typescript
// .vitepress/config.mts
export default defineConfig({
// true: 支持亮/暗切换(默认)
// 'dark': 默认暗色,可切换
// 'force-dark': 强制暗色
// 'force-auto': 强制跟随系统
// false: 禁用暗色模式
appearance: true
})工作原理
VitePress 的暗色模式通过以下机制实现:
- 在
<html>标签上添加class="dark" - 使用 CSS 变量切换颜色方案
- 通过
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: '✗ '; }暗色模式下的色觉适配要点:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 链接与正文 | 蓝色链接对色弱用户不明显 | 添加下划线或加粗 |
| 成功/错误 | 红/绿色盲无法区分 | 使用图标 + 文字标签 |
| 图表颜色 | 多色系难以分辨 | 使用不同图案/形状辅助 |
| 悬停状态 | 颜色变化不明显 | 增加 transform 或 border 变化 |
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-dark | GitHub 风格 | 技术文档 |
material-theme-lighter / material-theme-palenight | Material 风格 | 现代感 |
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_color 和 background_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);
}相关链接
- CSS 变量覆盖 — 完整的 CSS 变量列表
- 默认主题配置 — 主题基础配置
- Vue 组件开发指南 — 开发自定义组件
- 主题性能优化 — 主题性能优化
- 无障碍优化 — 无障碍最佳实践