mcpField
D
dark-mode

dark-mode

github.com/arnavpuri设计与 UI17108 次安装4 周前更新
代码仓库

关于 dark-mode

dark-mode 是面向界面设计与前端开发的深色模式设计技能,涵盖色彩、表面层级、对比度与实现方式,并强调避免简单反转颜色。

主要功能

  1. 01依据设计上下文推导深色主题
  2. 02定义表面、文字与边框层级
  3. 03调整颜色饱和度并检查对比度
  4. 04提供 CSS、主题切换及 Tailwind 模式
  5. 05检查主题闪烁、动效与渲染效果

适用场景

  1. 01为现有浅色界面添加深色主题
  2. 02构建支持系统偏好或手动切换的主题
  3. 03检查深色界面的文字与控件对比度

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/arnavpuri/designskills --skill dark-mode

安装后在 Claude Code 中输入/dark-mode确认已加载。

详细介绍

深色模式设计

开始之前:加载设计上下文

  1. 阅读 .agents/design-context.md(由 design-context 技能编写)。如果文件不存在,请查找 tailwind.config.* 中的令牌或 :root CSS 变量。如果都没有,请使用默认值(主色 #2563EB、次要色 #7C3AED、强调色 #F59E0B、Tailwind 灰色中性色、标题和正文使用 Inter、简约风格),并告知用户使用了默认值——建议运行 design-context。
  2. 将令牌映射到 CSS 变量一次,之后只使用这些变量:--color-primary、--color-primary-light、--color-primary-dark、--color-secondary、--color-accent、--color-neutral-50…--color-neutral-900、--color-success/-warning/-error、--font-heading、--font-body、--font-mono、--radius,以及以 4px 为基准的间距比例。
  3. **Tailwind:**将相同的变量公开为 primary、primary-light、primary-dark、secondary、accent、neutral-*、font-heading、font-body(v3 使用 theme.extend,v4 使用 @theme)。以下示例中的 indigo-* / gray-* 类是这些令牌的占位符。
  4. **Figma:**如果用户提供 Figma URL,请通过 Figma MCP(get_design_context、get_screenshot)获取设计并进行匹配;对于该设计,它优先于上下文文件。
  5. 上下文颜色是浅色主题的颜色。请根据这些颜色推导深色值(第 3 节:提高亮度,降低色度),而不是另创一套配色;询问这是深色优先设计,还是要为现有浅色 UI 添加深色模式。

1. 核心原则:不要只做反转

反转颜色会产生刺眼、难以阅读的界面。深色模式需要经过深思熟虑的设计决策。

需要改变的内容
元素浅色模式深色模式
背景白色(#fff)深灰色(不是纯黑色)
文本接近黑色接近白色(不是纯白色)
边框Gray-200不透明度为 8–12% 的白色
阴影不透明度为 5–15% 的黑色几乎不可见,或不使用
主色完全饱和降低饱和度
图像完全亮度略微调暗
层级阴影更浅的表面颜色
避免
  • 在纯黑色 #000 上使用纯白色 #fff 文本(21:1——会让许多读者产生光晕或眩光,散光者尤为明显)。
  • 将纯黑色用作默认背景:这样无法体现层级;在 OLED 上滚动时还可能出现拖影。真正的黑色应仅用于明确提供的“OLED/真黑”选项。
  • 沿用浅色模式中的相同阴影值——阴影在深色表面上几乎不可见。

2. 通过亮度体现层级

在浅色模式中,层级通过阴影体现;在深色模式中,提升层级的表面颜色更浅。

表面层级
:root {
  /* Light mode */
  --bg-base: oklch(100% 0 0);         /* white */
  --bg-surface: oklch(100% 0 0);      /* white */
  --bg-elevated: oklch(100% 0 0);     /* white + shadow */
  --bg-overlay: oklch(100% 0 0);      /* white + deeper shadow */
}

[data-theme="dark"] {
  /* Dark mode — each level gets progressively lighter */
  --bg-base: oklch(13% 0.01 265);     /* darkest: page background */
  --bg-surface: oklch(18% 0.01 265);  /* cards, sidebar */
  --bg-elevated: oklch(23% 0.01 265); /* dropdowns, popovers */
  --bg-overlay: oklch(28% 0.01 265);  /* modals, tooltips */
}
实用示例
.card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
}
.dropdown {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
}
.modal {
  background: var(--bg-overlay);
}
Material Design 覆盖层模型(M2)

每个层级都会在 #121212 基础色上叠加一层白色:

  • 0dp(基础):0% · 1dp(卡片):5% · 4dp(应用栏):9%
  • 8dp(菜单):12% · 16dp(导航抽屉):15% · 24dp(对话框):16%

Material 3 以色调化的 surface-container 角色取代覆盖层(从最低到最高);上面的表面令牌遵循相同思路。


3. 色彩饱和度规则

降低深色背景的饱和度

深色背景上的明亮、高饱和度颜色会造成视觉振动和眼疲劳。

:root {
  --color-primary: oklch(60% 0.25 265);   /* Light mode: full saturation */
  --color-success: oklch(55% 0.20 155);
  --color-error: oklch(55% 0.22 25);
}

[data-theme="dark"] {
  --color-primary: oklch(72% 0.18 265);   /* Dark mode: lighter, less saturated */
  --color-success: oklch(70% 0.15 155);
  --color-error: oklch(70% 0.16 25);
}
规则
  • 深色模式下将亮度提高 10–15%。
  • 将色度(饱和度)降低 15–25%。
  • 带颜色的背景(例如徽标)应降低不透明度。
  • 调整后重新检查对比度:在白色背景上通过对比度检查的主色,在深色模式下搭配白色文本时通常会不达标——填充按钮应改用更浅的主色,并搭配深色文本(见第 8 节)。
/* Light mode badge */
.badge-success {
  background: oklch(95% 0.05 155);
  color: oklch(35% 0.15 155);
}

/* Dark mode badge */
[data-theme="dark"] .badge-success {
  background: oklch(35% 0.08 155);
  color: oklch(80% 0.12 155);
}

4. 文本对比度等级

深色背景上的白色透明度等级

在深色背景上使用不同透明度的白色来建立文本层级。

角色透明度使用场景
高强调87–90% (oklch(100% 0 0 / 0.87))标题、主要文本
中强调60–70% (oklch(100% 0 0 / 0.70))正文、次要文本、弱化文本
仅用于禁用状态38% (oklch(100% 0 0 / 0.38))已禁用的控件(不受对比度规则约束)

用户必须阅读的任何内容——包括承载信息的弱化说明文字和占位符——都需要与其表面的对比度达到 4.5:1。在典型的深色表面上,白色透明度低于约 55% 时无法通过要求。

实现

使用下方完整令牌系统中的 --text-primary / --text-secondary / --text-muted 令牌(--text-muted 的亮度为 62%,这是在基础颜色和表面颜色上保持 4.5:1 对比度的最低值)。

Tailwind 方案

<h1 class="text-white/90">Main Heading</h1>


<p class="text-white/70">Body text content here.</p>


<span class="text-white/60">Last updated 2 hours ago</span>

5. 边框与分隔线处理

深色背景上的浅色边框

深色模式下的边框应使用半透明白色,而非灰色值。

[data-theme="dark"] {
  --border: oklch(100% 0 0 / 0.08);       /* subtle */
  --border-strong: oklch(100% 0 0 / 0.15); /* emphasized */
  --divider: oklch(100% 0 0 / 0.06);       /* section dividers */
}

这样可以确保边框无论底层表面颜色如何,都能正确适配。装饰性分隔线可以如此浅淡,但输入框和控件边框需要达到 3:1 的对比度(WCAG 1.4.11)——表单字段应使用 oklch(100% 0 0 / 0.3) 或更高的透明度。


6. 深色模式下的图像处理

降低图像亮度
[data-theme="dark"] img:not([data-no-dim]) {
  filter: brightness(0.85);
}

/* Restore on hover for galleries */
[data-theme="dark"] img:hover {
  filter: brightness(1);
}
为主视觉图像添加深色叠层
<div class="relative">
  <img src="/hero.jpg" alt="" class="w-full" />
  <div class="absolute inset-0 bg-gray-950/30 dark:bg-gray-950/50"></div>
</div>
SVG 图标适配

图标应使用 currentColor,以便自动继承文本颜色:

<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="currentColor">
  
</svg>
Logo 处理

为浅色和深色模式分别提供 Logo:

<img src="/logo-dark.svg" alt="Logo" class="dark:hidden" />
<img src="/logo-light.svg" alt="Logo" class="hidden dark:block" />

7. CSS 实现

始终设置:color-scheme 属性

此属性会告知浏览器以正确的主题渲染原生控件、滚动条、表单字段和默认画布——如果没有此属性,深色模式下的 <select> 和滚动条仍会保持浅色。

:root { color-scheme: light dark; }           /* follow the system */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

还应在 <head> 中添加 <meta name="color-scheme" content="light dark">,确保 CSS 加载前页面背景颜色正确。

方法 1:prefers-color-scheme(系统偏好设置)
:root {
  --bg: oklch(100% 0 0);
  --text: oklch(15% 0 0);
  --surface: oklch(100% 0 0);
  --border: oklch(90% 0 0);
}

@media (prefers-color-scheme: dark) {
  /* :not() lets a manual "light" choice (Method 2) override the system */
  :root:not([data-theme="light"]) {
    --bg: oklch(13% 0.01 265);
    --text: oklch(95% 0 0);
    --surface: oklch(18% 0.01 265);
    --border: oklch(100% 0 0 / 0.08);
  }
}
方法 2:CSS 自定义属性 + 类切换(用户偏好设置)
:root {
  --bg: oklch(100% 0 0);
  --text: oklch(15% 0 0);
}

[data-theme="dark"] {
  --bg: oklch(13% 0.01 265);
  --text: oklch(95% 0 0);
}

body {
  background: var(--bg);
  color: var(--text);
}
// Toggle implementation — keeps data-theme, the Tailwind `dark` class, and the button state in sync
function applyTheme(theme) {
  const root = document.documentElement;
  root.setAttribute('data-theme', theme);
  root.classList.toggle('dark', theme === 'dark');
  document.querySelectorAll('[data-theme-toggle]')
    .forEach((btn) => btn.setAttribute('aria-pressed', String(theme === 'dark')));
}
function toggleTheme() {
  const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  applyTheme(next);
  try { localStorage.setItem('theme', next); } catch (e) { /* storage blocked: session-only */ }
}
// Follow system changes until the user makes an explicit choice
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  let saved = null;
  try { saved = localStorage.getItem('theme'); } catch (e) {}
  if (!saved) applyTheme(e.matches ? 'dark' : 'light');
});

初始主题由下方的内联 <head> 脚本设置,而不是由页面加载后运行的函数设置。可以考虑提供三种选项(浅色 / 深色 / 系统)——“系统”会清除已保存的值。

方法 3:Tailwind dark: 变体
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
  border-gray-200 dark:border-white/10">
  <h2 class="text-gray-950 dark:text-white">Title</h2>
  <p class="text-gray-600 dark:text-gray-400">Description</p>
</div>

基于类的深色模式 Tailwind 配置:

// tailwind.config.js (v3.4.1+)
module.exports = {
  darkMode: 'selector',                          // `.dark` on <html>
  // darkMode: ['selector', '[data-theme="dark"]'], // or drive it from data-theme
}
/* Tailwind v4: in your main CSS file */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
防止错误主题闪烁(FOUC)

在任何样式表之前,将此脚本作为内联阻塞脚本添加到 <head> 中(不要使用 defer / async,也不要使用打包后的模块——它们会在首次绘制后运行):

<script>
  (function () {
    var theme = null;
    try { theme = localStorage.getItem('theme'); } catch (e) {}
    if (!theme) theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    var root = document.documentElement;
    root.setAttribute('data-theme', theme);
    root.classList.toggle('dark', theme === 'dark');
  })();
</script>

对于 SSR 框架(如 Next.js),在 <html> 上添加 suppressHydrationWarning,因为该属性与服务器渲染结果不同。


8. 组件专属的深色模式模式

输入框
<input class="bg-white border-gray-300 text-gray-900 placeholder:text-gray-400
  focus:border-indigo-500 focus:ring-indigo-500/20
  dark:bg-white/5 dark:border-white/30 dark:text-gray-100 dark:placeholder:text-gray-400
  dark:focus:border-indigo-400 dark:focus:ring-indigo-400/20" />
卡片
<div class="bg-white border border-gray-200 shadow-sm
  dark:bg-white/5 dark:border-white/10 dark:shadow-none">
按钮


<button class="bg-indigo-600 text-white hover:bg-indigo-500
  dark:bg-indigo-400 dark:text-indigo-950 dark:hover:bg-indigo-300">
  Primary
</button>


<button class="bg-gray-100 text-gray-700 hover:bg-gray-200
  dark:bg-white/10 dark:text-gray-200 dark:hover:bg-white/15">
  Secondary
</button>
下拉菜单 / 弹出层
<div class="bg-white border border-gray-200 shadow-lg rounded-xl
  dark:bg-gray-800 dark:border-white/10 dark:shadow-2xl dark:shadow-black/20">
表格
<tr class="border-b border-gray-100 hover:bg-gray-50
  dark:border-white/5 dark:hover:bg-white/5">

9. 平滑切换模式

模式间的 CSS 过渡

不要在 CSS 中添加永久性的 * { transition: … } 规则——它会覆盖每个组件自身的过渡效果(按钮变换、面板滑动),还会在首次绘制时为颜色添加动画。仅在切换期间应用临时类:

@media (prefers-reduced-motion: no-preference) {
  .theme-transition,
  .theme-transition *,
  .theme-transition *::before,
  .theme-transition *::after {
    transition: background-color 200ms ease, border-color 200ms ease,
      color 200ms ease, fill 200ms ease, stroke 200ms ease !important;
  }
}
// In toggleTheme(), wrap the switch:
const root = document.documentElement;
root.classList.add('theme-transition');
applyTheme(next);
setTimeout(() => root.classList.remove('theme-transition'), 250);

另一种方案:在支持的环境中使用 document.startViewTransition(() => applyTheme(next)) 实现交叉淡化。

切换按钮 UI

<button type="button" data-theme-toggle aria-pressed="false" aria-label="Dark mode"
  onclick="toggleTheme()" class="relative w-14 h-7 rounded-full
  bg-gray-500 dark:bg-indigo-500 transition-colors
  focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
  <span class="absolute top-0.5 left-0.5 w-6 h-6 rounded-full bg-white shadow
    motion-safe:transition-transform dark:translate-x-7 flex items-center justify-center">
    
    <svg class="w-4 h-4 text-amber-500 dark:hidden" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
      <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
    </svg>
    
    <svg class="w-4 h-4 text-indigo-500 hidden dark:block" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
      <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
    </svg>
  </span>
</button>

完整令牌系统

推荐的深色模式调色板

供所有 UI 技能共享(dashboard-design 也会复用这些值)。根据第 3 节中各部分的设计上下文值推导 --color-primary 和状态颜色;下方的 oklch 值是 Primary #2563EB 的默认值。

[data-theme="dark"] {
  color-scheme: dark;

  /* Surfaces */
  --bg-base: oklch(13% 0.01 265);
  --bg-surface: oklch(18% 0.01 265);
  --bg-elevated: oklch(23% 0.01 265);
  --bg-overlay: oklch(28% 0.01 265);

  /* Text */
  --text-primary: oklch(95% 0 0);
  --text-secondary: oklch(75% 0 0);
  --text-muted: oklch(62% 0 0);        /* ≥ 4.5:1 on base and surface */

  /* Borders */
  --border: oklch(100% 0 0 / 0.08);     /* decorative dividers */
  --border-strong: oklch(100% 0 0 / 0.15);
  --border-input: oklch(100% 0 0 / 0.30); /* form controls: ≥ 3:1 */

  /* Interactive */
  --color-primary: oklch(72% 0.18 265);
  --color-primary-hover: oklch(78% 0.16 265);

  /* Status */
  --color-success: oklch(70% 0.15 155);
  --color-warning: oklch(75% 0.15 80);
  --color-error: oklch(70% 0.16 25);
  --color-info: oklch(72% 0.15 230);
}

验证:渲染与检查

不要交付未经检查的代码。生成代码后:

  1. 在移动端和桌面端宽度下截图,例如运行 npx playwright screenshot --full-page --viewport-size=390,844 file://$PWD/index.html mobile.png,然后再用 --viewport-size=1440,900 截图(也可以使用开发服务器 URL)。
  2. 打开两张图片并检查:没有水平滚动或文本被裁切、重叠;层级一目了然;品牌设计令牌已应用;图片按正确的宽高比加载。
  3. 完整按一次 Tab 键:每个交互元素都显示清晰的焦点环,且顺序合乎逻辑。
  4. 截取两种主题的图片(Playwright --color-scheme=dark / light),在深色模式下重新加载,确认不会闪现浅色主题,并检查深色模式下次要文本和边框的对比度。
  5. 修复发现的问题,并在报告完成前重新截图。

深色模式检查清单

  • 背景为深灰色,文本为灰白色(避免纯 #fff 显示在纯 #000 背景上)
  • 已设置 color-scheme(CSS 属性和 meta 标签),使原生控件的外观保持一致
  • 通过较亮的表面体现层次,而非使用阴影
  • 主色已降低饱和度并提亮
  • 边框使用半透明白色
  • 图片略微调暗
  • Logo 提供浅色版本
  • 页面加载时不会闪现错误的主题(使用内联阻塞式 <head> 脚本)
  • 检测并遵循系统偏好设置
  • 用户偏好设置保存在 localStorage 中
  • 主题过渡为临时效果(不要使用全局 * 规则),并在减少动态效果的设置下禁用
  • 深色模式下的对比度仍符合 WCAG AA:文本(包括次要文本)为 4.5:1,输入框边框、图标和焦点环为 3:1
  • 切换控件是真正的 <button>,带有无障碍名称和 aria-pressed
  • 已渲染并截取两种主题的图片(参见“验证”)

评论

支持 Markdown