09CSS 变量与主题切换
同一个颜色在样式表里写了五十遍,改一次要全局搜索替换——CSS 变量(CSS Variables,
官方叫自定义属性 Custom Properties)就是来终结这种日子的:
一处定义、处处引用、改一处全站生效。其实你现在看的这个网站就是案例——
本站的 learn.css 里到处是 var(--accent)、var(--ink)。
学会它,你也能给自己的站点加上「一键换肤」。
一、语法第一课:--名: 值 定义,var(--名) 使用
规则只有两条:以双横线开头的属性就是变量(如 --brand: #C08A3E),
用 var(--brand) 在任意属性值里引用。
定义在 :root(即 html 元素)上,全站任何地方都能用——这是惯例做法。
变量名区分大小写,可以存颜色、长度、圆角……几乎任何属性值片段。
:root 里的 --brand 改成 #4E7A52,看三处颜色一键全变标题、边框、图标色都来自同一个变量
我没有写死任何颜色
改 :root 一处,我们仨一起变
var(--brand) 在计算时会被替换成
#C08A3E。所以它不能当属性名用(var(--side): 10px 是错的),只能出现在「值」的位置。二、作用域与继承:变量沿 DOM 树向下流动
变量天生支持级联(Cascade)与继承(Inheritance): 定义在哪个元素上,它的所有后代都能用;在子元素上重新赋值同名变量, 只影响这棵子树,容器外面纹丝不动。这正是「局部换肤」的原理。
.warm 里的 --brand 改成 #B3472E,只有圈内两张卡变色,圈外的不动三、var() 的两个保险:回退值与「非法陷阱」
var() 接受第二个参数作为回退值(Fallback):
变量没定义时用备胎,如 var(--x, #C08A3E)。
这对「组件允许外部定制、但要有默认样」的场景极其有用。回退值里还能再嵌套 var(),形成多级备胎。
--bg 根本没定义过,盒子用了备胎色。在 :root 里补一行 --bg: #F6E8DF;,看备胎被替换background: var(--bg, #F3EFE6) → 用了备胎 #F3EFE6
var(--a, var(--b, #96652C)) → 两个都没定义,兜底 #96652C
color: var(--c) 且 --c: 20px(颜色不合法),
文字不会保持上一个 color,而是变成继承来的颜色,常常「莫名其妙变黑」。
普通写错(color: 20px)会直接丢声明,两者行为完全不同,调试时尤其容易懵。| 情形 | 结果 | 示例 |
|---|---|---|
| 变量未定义 + 有回退值 | 用回退值 | 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 序号给名片里的技能标签加阶梯入场动画。全程不许出现写死的颜色。