首页
/ Bulma 深色模式(Dark Mode)实现指南:基于 prefers-color-scheme 与 CSS 变量的主题切换机制

Bulma 深色模式(Dark Mode)实现指南:基于 prefers-color-scheme 与 CSS 变量的主题切换机制

2026-09-06 21:40:06作者:咎岭娴Homer

Bulma 的深色模式不是简单的“换一套颜色”,而是一套由 prefers-color-scheme 媒体查询、data-theme 属性选择器和 CSS 变量(CSS Variables)三者协同驱动的主题系统。本文基于 Bulma 官方文档 docs/documentation/features/dark-mode.md,结合 sass/themes/dark.scsssass/utilities/css-variables.scss 等仓库源码,完整讲清楚深色模式的启用方式、底层变量注册机制,以及如何为自己的项目编写自定义主题。

1. 工作原理:系统偏好检测与强制切换的双轨机制

现代浏览器可以通过 CSS 媒体查询关键字 prefers-color-scheme 检测用户是否在系统中将主题偏好设置为 lightdark。这个值可以在媒体查询中使用,从而按用户偏好切换网站样式:

@media (prefers-color-scheme: dark) {
  :root {
    /* Update CSS variables */
  }
}

然而,网站本身无法通过这种方式“改变”用户的系统偏好(它只能“读取”)。因此 Bulma 在跟随系统偏好的同时,额外提供了两种强制手段:在 HTML 元素上设置 data-theme 属性,或添加 theme-dark 类。Bulma 定义深色主题时的完整选择器结构如下:

@media (prefers-color-scheme: dark) {
  :root {
    /* Update CSS variables */
  }
}

[data-theme=dark],
.theme-dark {
  /* Update CSS variables */
}

由此得到三条明确的行为规则:

  • 未设置任何用户偏好时,网站默认浅色(light)
  • 用户偏好为 light 时,网站保持浅色
  • 用户偏好为 dark 时,网站切换为深色

仓库中 sass/themes/_index.scss 展示了这套机制的完整落地,共注册了四个规则块:

/* Bulma Themes */
@use "../utilities/initial-variables" as iv;
@use "../utilities/css-variables" as cv;

@use "light";
@use "dark";
@use "setup";

// 1) 默认(无偏好时)::root 挂浅色主题
#{iv.$variables-host} {
  @include light.light-theme;
  @include setup.setup-theme;
}

// 2) 用户偏好 light
@include cv.system-theme($name: "light") {
  @include light.light-theme;
}

// 3) 用户偏好 dark
@include cv.system-theme($name: "dark") {
  @include dark.dark-theme;
}

// 4) 强制选择器::root, [data-theme=light], .theme-light
@include cv.bulma-theme($name: "light") {
  @include light.light-theme;
  @include setup.setup-theme;
}

// [data-theme=dark], .theme-dark
@include cv.bulma-theme($name: "dark") {
  @include dark.dark-theme;
  @include setup.setup-theme;
}

其中 $variables-host 默认为 ":root"(定义在 sass/utilities/initial-variables.scss),因此 system-theme 生成的变量会挂在 :root 上,与强制选择器形成“系统偏好”与“页面级覆盖”的双轨结构。

2. 页面内启用深色模式:局部强制与整页强制

2.1 对页面的一部分启用深色模式

无需任何 JavaScript,只要给任意 HTML 容器加上 data-theme="dark" 属性或 theme-dark 类,该容器内的 Bulma 组件就会使用深色变量:

<div>
  This is in Light Mode if the user hasn't set a preference,
  or if their preference is set to <code>light</code>.
</div>

<div data-theme="dark">
  This is in Dark Mode
</div>

<div class="theme-dark">
  This is also in Dark Mode
</div>

由于深色变量通过 CSS 自定义属性继承,任何嵌套的 .section.card.navbar 等组件都会自动读取作用域内的 --bulma-* 变量,实现局部主题隔离。

2.2 对整个网页启用深色模式

若希望整页进入深色模式,只需把属性或类设置在 <html> 根元素上:

<html data-theme="dark">
<!-- 或者 -->
<html class="theme-dark">

Bulma 官网自身就是一个真实用例:其主题切换脚本 docs/assets/javascript/main.js 中,setTheme() 函数正是通过 document.documentElement.setAttribute("data-theme", theme) 写入属性、并在选择“跟随系统”时调用 removeAttribute("data-theme") 恢复系统偏好,同时用 localStorage 持久化用户选择:

const setTheme = (theme, save = true) => {
  state.chosenTheme = theme;
  state.appliedTheme = theme;

  if (theme === SYSTEM_THEME) {
    state.appliedTheme = state.OSTheme;
    document.documentElement.removeAttribute("data-theme");
    window.localStorage.removeItem(STORAGE_KEY);
  } else {
    document.documentElement.setAttribute("data-theme", theme);
    if (save) {
      window.localStorage.setItem(STORAGE_KEY, theme);
    }
  }
  updateThemeUI();
};

3. 深色主题的源码剖析:sass/themes/dark.scss

深色主题的全部定义位于 sass/themes/dark.scss。文档中的示例代码与仓库当前实现略有出入(文档示例中 $scheme-main-l11%,当前仓库源码为 9%,且源码中新增了对 list 型颜色及 soft/bold 变量的处理),本文以仓库源码为准:

@use "sass:list";
@use "sass:meta";

@use "../utilities/initial-variables" as iv;
@use "../utilities/css-variables" as cv;
@use "../utilities/derived-variables" as dv;
@use "setup";

// The main lightness of this theme
$scheme-main-l: 9%;
$background-l: 14%;
$text-l: 71%;

// The main scheme color, used to make calculations
$scheme-main: hsl(iv.$scheme-h, iv.$scheme-s, $scheme-main-l);
$background: hsl(iv.$scheme-h, iv.$scheme-s, $background-l);
$text: hsl(iv.$scheme-h, iv.$scheme-s, $text-l);

@mixin dark-theme {
  @each $name, $color in dv.$colors {
    $base: $color;

    @if meta.type-of($color == "list") {
      $base: list.nth($color, 1);
    }

    @include cv.generate-on-scheme-colors($name, $base, $scheme-main);
  }

  @include cv.register-vars(
    (
      "scheme-brightness": "dark",
      "scheme-main-l": $scheme-main-l,
      "scheme-main-bis-l": $scheme-main-l + 2%,
      "scheme-main-ter-l": $scheme-main-l + 4%,
      "soft-l": iv.$dark-l,
      "bold-l": iv.$light-l,
      "soft-invert-l": iv.$light-l,
      "bold-invert-l": iv.$dark-l,
      "background-l": $background-l,
      "border-weak-l": 21%,
      "border-l": 24%,
      "text-weak-l": 53%,
      "text-l": $text-l,
      "text-strong-l": 93%,
      "text-title-l": 100%,
      "hover-background-l-delta": 5%,
      "active-background-l-delta": 10%,
      "hover-border-l-delta": 10%,
      "active-border-l-delta": 20%,
      "hover-color-l-delta": 5%,
      "active-color-l-delta": 10%,
    )
  );

  @include cv.register-hsl("shadow", white);
}

各部分职责如下:

  1. 三个核心亮度变量$scheme-main-l: 9%(页面主背景)、$background-l: 14%body 背景)、$text-l: 71%(正文文字),色相与饱和度沿用全局的 iv.$scheme-h(默认 221)与 iv.$scheme-s(默认 14%,见 sass/utilities/initial-variables.scss),保证深浅两种模式色彩家族一致;
  2. @each dv.$colors 循环:为 sass/utilities/derived-variables.scss 中定义的全部主色重新生成 on-scheme 变体(原理见第 5 节)。源码中对 $color 为 list 的情况先取 list.nth($color, 1) 作为基础色,这是比文档示例更健壮的写法;
  3. register-vars() 注册:一次性写入 scheme-brightness(取值 "dark",供组件判断明暗)及所有亮度变量;
  4. register-hsl("shadow", white):把阴影色从默认的黑色反转为白色,避免深色背景下出现难以察觉的黑影。

3.1 亮度反转策略与完整取值对照

对 Dark Mode,Bulma 保留主 scheme 颜色的色相与饱和度,但对背景、边框、文字及 hover/active 状态反转亮度。结合 sass/themes/light.scsssass/themes/dark.scss 的源码,完整对照如下(文档示例以 scheme-main-l: 11% 计,当前仓库源码为 9%):

CSS 变量 Light Mode Dark Mode
--bulma-scheme-main-l 100% 9%(文档示例 11%
--bulma-scheme-main-bis-l 98% $scheme-main-l + 2%
--bulma-scheme-main-ter-l 96% $scheme-main-l + 4%
--bulma-background-l 96% 14%
--bulma-border-weak-l 93% 21%
--bulma-border-l 86% 24%
--bulma-text-weak-l 48% 53%
--bulma-text-l 29% 71%
--bulma-text-strong-l 21% 93%
--bulma-text-title-l 14% 100%

hover/active 状态的亮度增量(delta)在两种模式下数值相同、符号相反——浅色模式为负值(变暗:hover-background-l-delta: -5%active-background-l-delta: -10%hover-border-l-delta: -10%active-border-l-delta: -20%hover-color-l-delta: -5%active-color-l-delta: -10%,见 sass/themes/light.scss),深色模式则全部反转为正值(+5% / +10% / +10% / +20% / +5% / +10%),即“悬停变亮”。这保证了交互反馈在两种底色上都符合直觉。

4. 三个核心 Sass Mixin:bulma-theme()system-theme()register-vars()

4.1 bulma-theme($name):生成强制主题选择器

源码位于 sass/utilities/css-variables.scss

@mixin bulma-theme($name) {
  [data-#{iv.$class-prefix}theme="#{$name}"],
  .#{iv.$class-prefix}theme-#{$name} {
    @content;
  }
}

它根据 $name 参数生成“HTML 属性选择器 + CSS 类选择器”的规则集:

@use "sass/utilities/css-variables" as cv;

@include cv.bulma-theme($name: "my-theme") {
  // Your code
}

编译输出:

[data-theme=my-theme],
.theme-my-theme {
  /* Your code */
}

注意选择器中的类名前缀由 iv.$class-prefix(默认空字符串,sass/utilities/initial-variables.scss)控制,因此与 versions/bulma-prefixed.scss 的带前缀版本(如 bulma-theme-dark)天然兼容。

4.2 system-theme($name):生成系统偏好媒体查询

源码位于 sass/utilities/css-variables.scss

@mixin system-theme($name) {
  @media (prefers-color-scheme: #{$name}) {
    #{iv.$variables-host} {
      @content;
    }
  }
}

输出示例($variables-host 默认为 :root):

@media (prefers-color-scheme: dark) {
  :root {
    /* Your code */
  }
}

4.3 register-vars($vars):批量注册带前缀的 CSS 变量

Bulma 的所有 CSS 变量都以 bulma- 为前缀,该前缀由 Sass 变量 $cssvars-prefix: "bulma-" 定义(sass/utilities/initial-variables.scss)。由于手写全部前缀很繁琐,Bulma 提供 register-vars() 接受一个 name: value 的 Sass map 批量注册。其实现是对 map 逐项调用 register-var(),由 buildVarName() 拼接出 --bulma-xxx 变量名(sass/utilities/css-variables.scss):

@function buildVarName($name, $prefix: "", $suffix: "") {
  @return "--#{iv.$cssvars-prefix}#{$prefix}#{$name}#{$suffix}";
}

@mixin register-vars($vars, $prefix: "", $suffix: "") {
  @each $name, $value in $vars {
    @include register-var($name, $value, $prefix, $suffix);
  }
}

完整用法示例(自定义主题中注册深色亮度变量):

@use "sass/utilities/css-variables" as cv;

$scheme-main-l: 11%;
$background-l: 14%;
$text-l: 71%;

@include cv.bulma-theme($name: "my-theme") {
  @include cv.register-vars(
    (
      "scheme-brightness": "dark",
      "scheme-main-l": $scheme-main-l,
      "scheme-main-bis-l": $scheme-main-l + 2%,
      "scheme-main-ter-l": $scheme-main-l + 4%,
      "background-l": $background-l,
      "border-weak-l": 21%,
      "border-l": 24%,
      "text-weak-l": 53%,
      "text-l": $text-l,
      "text-strong-l": 93%,
      "text-title-l": 100%,
      "hover-background-l-delta": 5%,
      "active-background-l-delta": 10%,
      "hover-border-l-delta": 10%,
      "active-border-l-delta": 20%,
      "hover-color-l-delta": 5%,
      "active-color-l-delta": 10%,
    )
  );
}

4.4 组合使用:让深色主题跟随系统偏好

要让你的主题响应 prefers-color-scheme,写法如下:

@use "sass/utilities/css-variables" as cv;
@use "sass/themes/dark";

@include cv.system-theme($name: "dark") {
  @include dark.dark-theme;
}

要同时支持 [data-theme=dark].theme-dark 强制选择器,则追加 setup-theme(原因见第 6 节):

@use "sass/utilities/css-variables" as cv;
@use "sass/themes/dark";
@use "sass/themes/setup";

@include cv.bulma-theme($name: "dark") {
  @include dark.dark-theme;
  @include setup.setup-theme;
}

5. 自动对比度系统:generate-on-scheme-colors()

scheme 颜色是 Bulma 用于页面背景、边框和文字色阶(strong / weak / title / 正文)的一组颜色。由于深色模式会反转 scheme 颜色的亮度——页面背景从白(100%)变为接近黑的深色,默认浅色模式下的 7 个主色(primary、link、info、success、warning、danger 及 text,定义于 sass/utilities/initial-variables.scss 等)在深底上的 -on-scheme 变体将不再可读,因此必须为每一个颜色重新生成。

generate-on-scheme-colors() 接受三个参数:

  • $name:颜色名称字符串,如 "primary"
  • $color:基础色值;
  • $scheme-main:主题的主 scheme 颜色(用作页面背景的那个)。

其实现是一套“可访问性对比度系统”(sass/utilities/css-variables.scss):先比较前景色与 scheme 背景色的相对亮度,若前景更亮则每轮加深 5%、否则每轮变浅 5%,最多迭代 20 轮,直到对比度比值 ($lum + 0.05) / ($lum + 0.05) 超过 5 为止;随后把得到的亮度注册为 --bulma-{$name}-on-scheme-l,并注册完整颜色 --bulma-{$name}-on-scheme

// 核心片段
@if ($fg-lum > $bg-lum) {
  @for $i from 0 through 20 {
    $ratio: math.div(($fg-lum + 0.05), ($bg-lum + 0.05));
    @if $ratio > 5 {
      $found-decent-color: true;
    } @else {
      $on-scheme-color: color.adjust($on-scheme-color, $lightness: 5%, $space: hsl);
      $fg-lum: fn.bulmaColorLuminance($on-scheme-color);
    }
  }
} @else {
  @for $i from 0 through 20 {
    // ... 亮度递减 5%,直到 ratio > 5
  }
}

自建主题时,对 dv.$colors 逐个颜色调用即可自动获得适配新背景色的 -on-scheme 变量:

@use "sass/utilities/css-variables" as cv;
@use "sass/utilities/derived-variables" as dv;

$scheme-main-l: 11%;
$scheme-main: hsl(iv.$scheme-h, iv.$scheme-s, $scheme-main-l);

@include cv.bulma-theme($name: "my-theme") {
  @each $name, $color in dv.$colors {
    @include cv.generate-on-scheme-colors($name, $color, $scheme-main);
  }
}

6. 为什么必须重新定义“计算型”变量:setup-theme()

Bulma 中部分 CSS 变量引用其它 CSS 变量。例如 --bulma-scheme-main 的定义是:

:root {
  --bulma-scheme-main: hsl(
    var(--bulma-scheme-h)
    var(--bulma-scheme-s)
    var(--bulma-scheme-main-l)
  );
}

由于 CSS 变量的求值时机,仅仅更新 --bulma-scheme-main-l 并不会让 --bulma-scheme-main 自动生效——必须在新作用域中重新定义它。这正是 setup-theme() 的职责:sass/themes/setup.scss 中的 setup-theme mixin 通过 register-vars() 把全部“计算型”变量用 hsl(var(...))calc(var(...) + var(...)) 形式重新注册,包括:

  • scheme-main / scheme-main-bis / scheme-main-ter:由 -l 亮度变量组合;
  • background / background-hover / background-active:hover/active 值用 calc(background-l + hover/active-background-l-delta) 计算;
  • border-weak / border / border-hover / border-active
  • text-weak / text / text-strong(使用独立的 text-h / text-s);
  • scheme-invert-ter / scheme-invert-bis / scheme-invert
  • link / link-text / link-text-hover / link-text-active(基于 link-on-scheme-l 与 color delta);
  • focus-* 焦点样式变量、code / pre 代码块颜色、shadow 阴影(由 shadow-h/s/l 组合)。

因此自建主题的完整调用顺序是:先注册自己的变量,再调用 setup.setup-theme()。编译输出示例:

[data-theme=my-theme],
.theme-my-theme {
  --bulma-scheme-main-l: 7%;
  --bulma-scheme-main: hsl(
    var(--bulma-scheme-h)
    var(--bulma-scheme-s)
    var(--bulma-scheme-main-l)
  );
}
@use "sass/themes/setup";

@include cv.bulma-theme($name: "my-theme") {
  // 方式一:手动写变量
  --bulma-scheme-main-l: 7%;
  // 方式二:用 register-vars() 批量注册
  // @include cv.register-vars(( "scheme-main-l": 7% ));

  @include setup.setup-theme();
}

7. 替代方案:bulma-no-dark-mode 构建

如果产品不需要深色模式,仓库提供了 versions/bulma-no-dark-mode.scss 这一替代入口:它同样转发 utilities、base、elements、form、components、grid、layout、skeleton 与 helpers,但主题部分:root 上注册 light-theme + setup-theme,不包含任何 system-themebulma-theme 规则,从而彻底移除深色模式相关 CSS,减小产物体积:

@use "../sass/themes/light";
@use "../sass/themes/setup";

:root {
  @include light.light-theme;
  @include setup.setup-theme;
}

小结

Bulma 深色模式的设计可归纳为三层:

  1. 触发层system-theme() 生成 @media (prefers-color-scheme: dark) { :root { ... } } 跟随系统;bulma-theme() 生成 [data-theme=dark], .theme-dark 选择器支持页面级强制切换,默认始终为浅色;
  2. 变量层register-vars()bulma- 前缀批量注册亮度变量,深色模式通过“保留色相饱和度、反转亮度、delta 变号”的策略完成明暗互换;
  3. 一致性层generate-on-scheme-colors() 以对比度 5:1 为目标的迭代算法保证 7 个主色在任意底色上可读,setup-theme() 则负责重定义所有引用其它变量的计算型颜色,确保变量修改后整条推导链正确生效。

理解这套机制后,你可以基于 sass/themes/ 的模式快速为自己的品牌色创建可跟随系统、可局部覆盖的完整主题。

登录后查看全文
热门项目推荐
相关项目推荐