Bulma 深色模式(Dark Mode)实现指南:基于 prefers-color-scheme 与 CSS 变量的主题切换机制
Bulma 的深色模式不是简单的“换一套颜色”,而是一套由 prefers-color-scheme 媒体查询、data-theme 属性选择器和 CSS 变量(CSS Variables)三者协同驱动的主题系统。本文基于 Bulma 官方文档 docs/documentation/features/dark-mode.md,结合 sass/themes/dark.scss、sass/utilities/css-variables.scss 等仓库源码,完整讲清楚深色模式的启用方式、底层变量注册机制,以及如何为自己的项目编写自定义主题。
1. 工作原理:系统偏好检测与强制切换的双轨机制
现代浏览器可以通过 CSS 媒体查询关键字 prefers-color-scheme 检测用户是否在系统中将主题偏好设置为 light 或 dark。这个值可以在媒体查询中使用,从而按用户偏好切换网站样式:
@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-l 为 11%,当前仓库源码为 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);
}
各部分职责如下:
- 三个核心亮度变量:
$scheme-main-l: 9%(页面主背景)、$background-l: 14%(body背景)、$text-l: 71%(正文文字),色相与饱和度沿用全局的iv.$scheme-h(默认221)与iv.$scheme-s(默认14%,见 sass/utilities/initial-variables.scss),保证深浅两种模式色彩家族一致; @each dv.$colors循环:为 sass/utilities/derived-variables.scss 中定义的全部主色重新生成on-scheme变体(原理见第 5 节)。源码中对$color为 list 的情况先取list.nth($color, 1)作为基础色,这是比文档示例更健壮的写法;register-vars()注册:一次性写入scheme-brightness(取值"dark",供组件判断明暗)及所有亮度变量;register-hsl("shadow", white):把阴影色从默认的黑色反转为白色,避免深色背景下出现难以察觉的黑影。
3.1 亮度反转策略与完整取值对照
对 Dark Mode,Bulma 保留主 scheme 颜色的色相与饱和度,但对背景、边框、文字及 hover/active 状态反转亮度。结合 sass/themes/light.scss 与 sass/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-theme 或 bulma-theme 规则,从而彻底移除深色模式相关 CSS,减小产物体积:
@use "../sass/themes/light";
@use "../sass/themes/setup";
:root {
@include light.light-theme;
@include setup.setup-theme;
}
小结
Bulma 深色模式的设计可归纳为三层:
- 触发层:
system-theme()生成@media (prefers-color-scheme: dark) { :root { ... } }跟随系统;bulma-theme()生成[data-theme=dark], .theme-dark选择器支持页面级强制切换,默认始终为浅色; - 变量层:
register-vars()以bulma-前缀批量注册亮度变量,深色模式通过“保留色相饱和度、反转亮度、delta 变号”的策略完成明暗互换; - 一致性层:
generate-on-scheme-colors()以对比度 5:1 为目标的迭代算法保证 7 个主色在任意底色上可读,setup-theme()则负责重定义所有引用其它变量的计算型颜色,确保变量修改后整条推导链正确生效。
理解这套机制后,你可以基于 sass/themes/ 的模式快速为自己的品牌色创建可跟随系统、可局部覆盖的完整主题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00