09CSS 变量与主题切换

同一个颜色在样式表里写了五十遍,改一次要全局搜索替换——CSS 变量(CSS Variables, 官方叫自定义属性 Custom Properties)就是来终结这种日子的: 一处定义、处处引用、改一处全站生效。其实你现在看的这个网站就是案例—— 本站的 learn.css 里到处是 var(--accent)、var(--ink)。 学会它,你也能给自己的站点加上「一键换肤」。

约 20 分钟 6 个动手实验 换肤基本功

一、语法第一课:--名: 值 定义,var(--名) 使用

规则只有两条:以双横线开头的属性就是变量(如 --brand: #C08A3E), 用 var(--brand) 在任意属性值里引用。 定义在 :root(即 html 元素)上,全站任何地方都能用——这是惯例做法。 变量名区分大小写,可以存颜色、长度、圆角……几乎任何属性值片段。

三张卡片共用一个变量:把 :root 里的 --brand 改成 #4E7A52,看三处颜色一键全变
变量值就是一段「文本替换」:var(--brand) 在计算时会被替换成 #C08A3E。所以它不能当属性名用(var(--side): 10px 是错的),只能出现在「值」的位置。

二、作用域与继承:变量沿 DOM 树向下流动

变量天生支持级联(Cascade)与继承(Inheritance): 定义在哪个元素上,它的所有后代都能用;在子元素上重新赋值同名变量, 只影响这棵子树,容器外面纹丝不动。这正是「局部换肤」的原理。

局部覆盖现场:把 .warm 里的 --brand 改成 #B3472E,只有圈内两张卡变色,圈外的不动
覆盖查找规则:用变量时,浏览器从当前元素一路向上找最近的定义。 所以「越靠近使用处的定义赢」,和 JavaScript 的作用域链很像。

三、var() 的两个保险:回退值与「非法陷阱」

var() 接受第二个参数作为回退值(Fallback): 变量没定义时用备胎,如 var(--x, #C08A3E)。 这对「组件允许外部定制、但要有默认样」的场景极其有用。回退值里还能再嵌套 var(),形成多级备胎。

回退值演示:--bg 根本没定义过,盒子用了备胎色。在 :root 里补一行 --bg: #F6E8DF;,看备胎被替换
大坑预警:变量值写错不会「整条声明作废」,而是触发 「计算值时非法」(Invalid at computed-value time)——属性会回落到 继承值或初始值。比如 color: var(--c) 且 --c: 20px(颜色不合法), 文字不会保持上一个 color,而是变成继承来的颜色,常常「莫名其妙变黑」。 普通写错(color: 20px)会直接丢声明,两者行为完全不同,调试时尤其容易懵。
var() 异常情形速查:备胎何时登场
情形结果示例
变量未定义 + 有回退值用回退值var(--x, 12px) → 12px
变量未定义 + 无回退值属性变「计算值时非法」→ 继承值 / 初始值color: var(--x) → 继承父级颜色
变量值类型不对同上(不是丢声明!)color: var(--w) 且 --w: 20px → 继承值
回退值嵌套层层兜底var(--a, var(--b, red))

四、命名与组织:设计令牌(Design Token)思想

变量一多,命名就是工程问题。业界通行的做法是设计令牌(Design Token): 把颜色、间距、圆角、阴影这些「设计决策」分层命名——底层是原始值,上层是语义化用途, 组件只引用语义层,换主题时只动语义层。

令牌命名规范建议:前缀分层,见名知意
类别建议前缀示例说明
颜色--c---c-brand、--c-bg-page、--c-text-2按「用途」命名而非按颜色命名(别叫 --c-orange)
间距--space---space-xs:4px、--space-md:16px固定几个档位,页面节奏立刻统一
圆角--r---r-card:12px、--r-btn:8px按组件角色命名
阴影--shadow---shadow-1、--shadow-pop层级从低到高编号
字号 / 字重--fs- / --fw---fs-body:15px、--fw-title:700排版令牌
组件私有--组件---btn-bg、--card-pad只在组件内部用,第六节实战
组织建议:全局令牌集中放 :root;主题差异单独一组 (如 body[data-theme="dark"] 里只覆盖语义色);组件私有变量写在组件选择器里。 三层各司其职,维护时不打架。

五、实战一·明暗主题切换

有了「局部覆盖」这把刀,主题切换就变得优雅:两套变量、一份结构。 默认主题写在 :root,暗色主题在 [data-theme="dark"] 里覆盖整套语义色, 切换时只需改一个属性。 本站实验台为安全沙箱禁用了脚本,下面用纯 CSS 的 checkbox 开关模拟切换; 真实项目里用一行 JS 改 body.dataset.theme 即可,原理完全一致。

一键换肤:点预览区里的「切换主题」开关看暖纸/夜幕两套配色互切;再把暗色的 --bg 改成 #3A2E22 看变化
工程上还常配合 prefers-color-scheme: dark 媒体查询做「跟随系统」, 再用 localStorage 记住用户手动选择。三者优先级:用户选择 > 跟随系统 > 默认。

六、实战二·组件级变量:改变量,不改规则

组件把自己的「可定制点」抽成私有变量(如 --btn-bg), 规则里只写 var()。之后做变体(危险按钮、大号按钮)时, 只声明新变量值,不重复任何规则——这就是「改变量不改结构」的派生玩法。

改变量派生变体:给 .btn-danger 加一行 --btn-pad: 6px 22px;,看危险按钮单独变胖,结构类一行没动
这套打法配合回退值更稳:background: var(--btn-bg, #C08A3E)—— 调用方不定义也能跑,定义了就定制。很多 CSS 框架的主题机制就是这个思路。

七、配合 calc():变量让计算「有名字」

变量和 calc() 是黄金搭档:把「基数」存进变量, 间距、尺寸、动画延迟都能按比例推导——改基数一处,整串派生值全变。 用序号变量 --i 还能做阶梯动画延迟(动画详见本章 07-动画一课)。

倍数间距 + 阶梯延迟:把 --n 改成 4,看间距全部按比例翻倍、入场动画节奏不变形
注意:calc(var(--n) * 8px) 里 --n 必须是纯数字 (写成 --n: 2px 再乘长度会算出「长度×长度」而非法)。 无单位数字变量 + calc 乘单位,是最灵活的用法。

八、本章小结

  • 双横线定义、var() 引用:--brand: #C08A3E + color: var(--brand);定义在 :root 全站可用。
  • 变量沿 DOM 树继承并级联,子元素覆盖同名变量只影响自己的子树——局部换肤的原理。
  • var(--x, 备胎) 提供回退;变量值类型不对会触发「计算值时非法」,属性回落到继承值 / 初始值而非丢声明。
  • 设计令牌分层命名:--c- 颜色、--space- 间距、--r- 圆角、--shadow- 阴影,按用途而非按值命名。
  • 主题切换 = body[data-theme="dark"] 覆盖整套语义变量,结构零改动。
  • 组件私有变量(如 --btn-bg)让变体「只改变量不改结构」。
  • 配 calc():纯数字变量做倍数间距与阶梯动画延迟,改基数全串联动。
动手练习:把你在第二章做的「个人名片」页面改造成变量驱动—— ① 抽出 --c-brand、--c-bg、--space-md、--r-card 四个令牌放 :root; ② 加一套 body[data-theme="dark"] 暗色覆盖和切换按钮; ③ 做一个 .btn 基类,只用变量派生出「关注」「取关」两个变体; ④ 用 --i 序号给名片里的技能标签加阶梯入场动画。全程不许出现写死的颜色。