教程

CSS light-dark() 函数:原生主题切换

阅读约 3 分钟

深色模式的实现历来需要大量样板代码:一个覆盖每个自定义属性的 prefers-color-scheme 媒体查询、用于处理用户切换的 JavaScript、用于持久化偏好设置的 localStorage,以及在 <head> 中内联脚本以防止页面加载时出现错误主题闪烁。CSS light-dark() 函数并不能消除所有这些,但它显著减少了该问题所涉及的 CSS 代码量。

light-dark() 是一个 CSS 颜色函数,接受恰好两个颜色值,在活动颜色方案为亮色时返回第一个,在深色时返回第二个。它是颜色领域的 CSS 语义版三元运算符。

什么是 light-dark()?

函数签名很简单:

color: light-dark(<light-color>, <dark-color>);

当活动颜色方案为亮色时,浏览器使用 <light-color>;为深色时,使用 <dark-color>。"活动颜色方案"由 color-scheme CSS 属性决定,而该属性响应系统的 prefers-color-scheme 媒体查询或在元素上设置的显式值。

该函数支持于:

  • Chrome/Edge:自版本 123(2024 年 3 月)起
  • Firefox:自版本 120(2023 年 11 月)起
  • Safari:自版本 17.5(2024 年 6 月)起

截至 2026 年初,全球支持率约为 85%。它是相对较新的功能,但浏览器覆盖率增长足够快,可以在有回退策略的情况下用于生产环境。

与 color-scheme 属性的工作原理

light-dark() 不能单独工作。它完全依赖于 color-scheme CSS 属性的正确设置。没有它,函数就没有上下文来决定返回哪个值。

color-scheme 属性声明文档或元素支持哪些颜色方案。在 :root 上设置它是起点:

:root {
  color-scheme: light dark;
}

这一声明告知浏览器你的页面同时支持亮色和深色颜色方案。浏览器随后:

  1. 读取用户的 prefers-color-scheme 系统偏好
  2. 应用相应的方案
  3. 让页面上所有 light-dark() 调用解析为适当的值

有了这个设置,定义主题感知颜色就变成了编写单一声明的事情:

:root {
  color-scheme: light dark;

  --color-background: light-dark(#FFFFFF, #0F0F17);
  --color-text:       light-dark(#1A1A2E, #E8E8F0);
  --color-border:     light-dark(#DEE2E6, #2E2E4A);
  --color-accent:     light-dark(#2563EB, #60A5FA);
}

无需媒体查询。无需选择器覆盖。每种颜色一个声明,两个值内联在一起。浏览器根据系统偏好自动处理切换。

限定为单一方案

color-scheme 设置为 lightdark 可以强制使用单一方案,无论系统偏好如何:

/* 无论操作系统偏好如何,始终为亮色 */
.widget {
  color-scheme: light;
  background: light-dark(#FFFFFF, #0F0F17);
  /* 始终解析为 #FFFFFF */
}

/* 始终为深色 */
.dark-panel {
  color-scheme: dark;
  color: light-dark(#1A1A2E, #E8E8F0);
  /* 始终解析为 #E8E8F0 */
}

这对于始终以特定模式显示的 UI 组件很有用——例如,无论周围页面主题如何,始终具有深色背景的代码编辑器。

only 关键字

添加 only 可防止层叠覆盖该元素的方案:

.forced-light {
  color-scheme: only light;
}

这主要在你有一个位于深色模式上下文内、但必须保持亮色的元素时使用。

替代 prefers-color-scheme 媒体查询

使用媒体查询实现深色模式的传统方法需要复制或覆盖每个颜色变量:

/* 传统方法——冗长 */
:root {
  --bg: #FFFFFF;
  --text: #1A1A2E;
  --accent: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F17;
    --text: #E8E8F0;
    --accent: #60A5FA;
  }
}

使用 light-dark(),这简化为:

/* light-dark() 方法——每个变量一个声明 */
:root {
  color-scheme: light dark;

  --bg:     light-dark(#FFFFFF, #0F0F17);
  --text:   light-dark(#1A1A2E, #E8E8F0);
  --accent: light-dark(#2563EB, #60A5FA);
}

变量本身变得自描述。当你阅读 --accent: light-dark(#2563EB, #60A5FA) 时,你立刻能看到两个值并理解它们的关系。媒体查询方法将亮色和深色值分散在两个独立的代码块中,使审计和更新调色板变得更加困难。

仍然需要媒体查询的场景

prefers-color-scheme 媒体查询对于根据主题变化的非颜色适配仍然必要:

@media (prefers-color-scheme: dark) {
  /* light-dark() 无法表达的非颜色调整 */
  img.logo {
    filter: invert(1) brightness(1.2);
  }

  .hero-image {
    opacity: 0.85;
  }
}

对于纯颜色更改,light-dark() 更简洁。对于结构性或非颜色适配(图像滤镜、不透明度、display 属性),媒体查询仍然是正确的工具。

与 CSS 自定义属性结合使用

light-dark() 可以在自定义属性值内使用,这正是其全部功能得以展现的地方。你在 :root 处定义所有主题感知颜色,页面中的每个组件都引用这些变量。颜色方案更改时,所有内容同步更新。

完整主题系统示例

:root {
  color-scheme: light dark;

  /* 背景 */
  --color-bg-base:      light-dark(#FFFFFF,   #0F0F17);
  --color-bg-elevated:  light-dark(#F8F9FA,   #1A1A2E);
  --color-bg-overlay:   light-dark(#F1F3F5,   #252540);

  /* 文字 */
  --color-text-primary:   light-dark(#1A1A2E, #E8E8F0);
  --color-text-secondary: light-dark(#4A4A6A, #A8A8C0);
  --color-text-muted:     light-dark(#6C757D, #6A6A88);
  --color-text-inverse:   light-dark(#FFFFFF, #1A1A2E);

  /* 边框 */
  --color-border:         light-dark(#DEE2E6, #2E2E4A);
  --color-border-strong:  light-dark(#ADB5BD, #4A4A6A);

  /* 交互/品牌 */
  --color-accent:         light-dark(#2563EB, #60A5FA);
  --color-accent-hover:   light-dark(#1D4ED8, #93C5FD);
  --color-accent-subtle:  light-dark(#DBEAFE, #1E3A5F);

  /* 反馈 */
  --color-success:  light-dark(#16A34A, #4ADE80);
  --color-warning:  light-dark(#D97706, #FCD34D);
  --color-danger:   light-dark(#DC2626, #F87171);

  --color-success-bg: light-dark(#F0FDF4, #052E16);
  --color-warning-bg: light-dark(#FFFBEB, #2D1A00);
  --color-danger-bg:  light-dark(#FEF2F2, #2D0A0A);
}

组件引用这些变量时无需了解任何主题相关信息:

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-inverse);
}

.btn-primary:hover {
  background: var(--color-accent-hover);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-left: 3px solid var(--color-success);
}

注意,亮色强调色 #2563EB 在深色模式下切换为 #60A5FA。这是有意为之——600 号蓝色在白色背景上通过 WCAG AA 对比度,但在深色背景上失败。400 号蓝色足够明亮,可以在深色表面上保持可访问的对比度。使用对比度检查器 验证每种组合是否满足目标对比度,使用色阶生成器 为每种模式找到合适的色阶。

在其他函数内嵌套 light-dark()

light-dark() 返回一个颜色值,因此可以在任何颜色有效的地方使用——包括在其他函数内部:

:root {
  color-scheme: light dark;
  --brand: #3B82F6;

  /* 在 color-mix() 内使用 light-dark() */
  --brand-surface: color-mix(
    in oklch,
    var(--brand) 15%,
    light-dark(white, #09090b)
  );
}

这创建了一个表面颜色,它是品牌色的 15% 色调,在亮色模式下与白色混合,在深色模式下与近黑色混合——自动具备主题感知能力。

添加用户切换(JavaScript)

响应系统偏好是正确的默认行为,但用户应该能够覆盖它。这需要 JavaScript 来持久化他们的选择并覆盖浏览器默认值。

通过 JavaScript 控制 color-scheme

关键认知是 color-scheme 是一个可以通过 JavaScript 设置的 CSS 属性:

// 以编程方式设置颜色方案
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';

// 移除覆盖(恢复为系统偏好)
document.documentElement.style.colorScheme = '';

当你通过内联样式在根元素上设置 color-scheme 时,它会覆盖样式表声明。所有 light-dark() 值重新解析为适当的变体。

完整的切换实现

const STORAGE_KEY = 'color-scheme-preference';

function initColorScheme() {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === 'light' || stored === 'dark') {
    document.documentElement.style.colorScheme = stored;
  }
  // 若无存储偏好,CSS color-scheme: light dark; 通过操作系统偏好自动处理
}

function toggleColorScheme() {
  const current = getComputedStyle(document.documentElement)
    .colorScheme
    .trim();

  // 确定下一个值
  const next = current.includes('dark') ? 'light' : 'dark';

  document.documentElement.style.colorScheme = next;
  localStorage.setItem(STORAGE_KEY, next);

  // 更新任何切换按钮状态
  updateToggleButton(next);
}

function updateToggleButton(scheme) {
  const btn = document.getElementById('theme-toggle');
  if (!btn) return;
  btn.setAttribute('aria-label',
    scheme === 'dark' ? '切换到亮色模式' : '切换到深色模式'
  );
  btn.dataset.scheme = scheme;
}

// 在第一次绘制前运行,避免闪烁
initColorScheme();

// DOM 就绪后绑定到切换按钮
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('theme-toggle')
    ?.addEventListener('click', toggleColorScheme);
});

在 DOM 完全解析之前调用 initColorScheme() 至关重要。如果运行过晚,有存储偏好的用户会短暂看到操作系统默认主题后再切换——经典的"错误主题闪烁"。将此脚本内联在 <head> 中,或谨慎使用 defer 属性(注意 defer 脚本在 DOM 解析后运行,可能太晚)。

防闪烁模式

最稳健的防闪烁方法在 <head> 中运行最小化内联脚本:

<head>
  <meta name="color-scheme" content="light dark">
  <script>
    const stored = localStorage.getItem('color-scheme-preference');
    if (stored) {
      document.documentElement.style.colorScheme = stored;
    }
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

此脚本在应用任何 CSS 之前同步运行,因此浏览器从第一次绘制就计算出正确的 color-scheme 值。<meta name="color-scheme"> 标签告知浏览器在 CSS 解析之前期待哪些方案——这可以防止某些浏览器在深色模式页面上出现短暂的白色闪烁。

从基于 JS 的主题迁移指南

许多现有的深色模式实现使用由 JavaScript 切换的 data-theme 属性,CSS 覆盖范围限定在 [data-theme="dark"]。迁移到 light-dark() 是渐进式的——你不需要一次性更改所有内容。

步骤一:为 :root 添加 color-scheme

:root {
  color-scheme: light dark;
  /* 现有自定义属性保持不变 */
}

步骤二:逐一迁移变量

从单个变量开始作为概念验证。将分散的声明模式替换为统一的 light-dark()

/* 之前 */
:root {
  --bg: #FFFFFF;
}
[data-theme="dark"] {
  --bg: #0F0F17;
}

/* 之后 */
:root {
  color-scheme: light dark;
  --bg: light-dark(#FFFFFF, #0F0F17);
}

步骤三:更新切换逻辑

将 JavaScript 切换从设置 data-theme 改为设置 style.colorScheme

/* 之前 */
document.documentElement.setAttribute('data-theme', scheme);

/* 之后 */
document.documentElement.style.colorScheme = scheme;

步骤四:移除 data-theme 选择器

所有变量迁移完成后,移除 [data-theme="dark"] CSS 代码块。

过渡期间并行运行两套系统

你可以同时运行两套系统。对于尚未迁移的变量,保留 [data-theme="dark"] 覆盖。新变量使用 light-dark()。在过渡期间,JavaScript 切换同时设置 data-themestyle.colorScheme

浏览器支持回退

对于大约 15% 不支持 light-dark() 的浏览器,提供显式回退:

:root {
  /* 回退:显式亮色模式值 */
  --color-bg: #FFFFFF;
  --color-text: #1A1A2E;

  /* 使用 light-dark() 的渐进增强 */
  --color-bg:   light-dark(#FFFFFF, #0F0F17);
  --color-text: light-dark(#1A1A2E, #E8E8F0);
}

/* 不支持 light-dark() 的浏览器的回退深色模式 */
@supports not (color: light-dark(white, black)) {
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg: #0F0F17;
      --color-text: #E8E8F0;
    }
  }
}

@supports not (color: light-dark(white, black)) 代码块只适用于不理解 light-dark() 的浏览器。现代浏览器完全跳过它,因为负条件为假。

核心要点

  • light-dark(<light-value>, <dark-value>) 在亮色方案中返回第一个参数,在深色方案中返回第二个。它是 CSS 原生表达"这种颜色,根据当前主题适配"的方式。
  • 它依赖于在元素(或祖先)上正确设置的 color-scheme CSS 属性。始终在 :root 上设置 color-scheme: light dark 以通过 prefers-color-scheme 实现自动适配。
  • 与传统媒体查询方式相比,主要优势在于将两种主题值放在单一声明中——使亮色和深色变体之间的关系明确可审计。
  • 用户覆盖需要通过 JavaScript 设置 document.documentElement.style.colorScheme。将选择持久化到 localStorage,并在 CSS 加载前通过 <head> 内联脚本应用,以防止闪烁。
  • data-theme 属性系统的迁移是渐进式的——每次将一个变量从 [data-theme="dark"] 覆盖模式迁移到 light-dark() 内联模式。
  • 截至 2026 年,浏览器支持率约为 85%。为较旧环境提供 @supports not 回退,结合 @media (prefers-color-scheme: dark) 代码块。
  • 使用对比度检查器 验证每个 light-dark() 对中的亮色和深色值都满足其对应背景的 WCAG 对比度要求;使用色阶生成器 为每种模式找到合适的色阶。

相关颜色

相关品牌

相关工具