CSS 变量实战:主题切换、calc 组合、继承回退与媒体查询里的限制

CSS 自定义属性远不止「存个颜色」:它能在 :root 与 data-theme 之间实现零 JS 主题切换,能和 calc 组合成设计令牌系统,也有一套值得背下来的继承与回退规则。同时它有明确的能力边界——不能在媒体查询的条件里使用。本文把这些实战模式与限制一次讲清,附可用代码。

约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

基础回顾:定义、读取、覆盖

变量在作用域内定义、沿 DOM 继承、可被后代覆盖。这与预处理器变量最大的不同:CSS 变量是运行时值,可以随 DOM 结构、媒体查询、甚至 JS 动态变化。记住读取语法 var(--name, fallback),fallback 只在变量未定义时生效。

定义与读取
:root {
  --brand: #10b981;
  --space: 8px;
}
.btn {
  background: var(--brand);
  padding: calc(var(--space) * 2) calc(var(--space) * 3);
  color: var(--brand-text, white); /* --brand-text 未定义时用 white */
}

主题切换:改一个属性,全站变色

把颜色抽到 :root,再为 data-theme="dark" 重定义同名变量,切换主题只需改 html 上的一个属性,零 JS 重绘逻辑、零样式重复。prefers-color-scheme 也可与 data-theme 叠加以支持「跟随系统 + 手动覆盖」三态。

零 JS 重绘的主题系统
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --card: #f5f5f5;
}
[data-theme="dark"] {
  --bg: #0f172a;
  --text: #e2e8f0;
  --card: #1e293b;
}
body {
  background: var(--bg);
  color: var(--text);
}
/* 切换:document.documentElement.dataset.theme = 'dark' */

calc 组合:一套间距/字号令牌

变量与 calc 的组合能从两三个基础值推导整套刻度,改基础值即全局生效。这是设计令牌的最小实现,比引入整套方案更轻。注意 calc 里变量与数字运算时需要带单位的部分乘数字、数字乘带单位部分,顺序无影响但不能除以带单位值。

间距刻度推导
:root {
  --base: 4px;
  --gap-sm: calc(var(--base) * 2);   /* 8px */
  --gap-md: calc(var(--base) * 4);   /* 16px */
  --gap-lg: calc(var(--base) * 8);   /* 32px */
}
.grid { gap: var(--gap-md); }
.section { margin-block: var(--gap-lg); }

继承与回退规则速查

变量的「怪行为」大多源于继承模型,记住这张表能避开 90% 的意外:

规则行为实战含义
子元素读不到时回退到祖先变量沿 DOM 向下继承在组件根上覆盖变量即可定制组件
var() 的 fallback 只在未定义时生效变量已定义但值非法不会触发 fallback非法值会导致属性回退到 initial/inherit,不是 fallback
变量名大小写敏感--Color 与 --color 是两个变量统一小写 kebab-case 命名
变量在元素自身作用域内即时解析在后代重定义立即生效于该后代子树主题可精确到单个组件甚至单个卡片
循环引用会使属性失效a 引 b、b 引 a 时两者均无效令牌分层:基础层不引用派生层

@property:给变量加上类型与过渡

普通变量是字符串,无法参与过渡动画。用 @property 注册后变量获得类型、初始值与继承语义,background 渐变角度、颜色等都能平滑动画。兼容性:Chrome/Edge/Safari 已支持,Firefox 近期版本也已落地,旧环境自动降级为无动画。

可动画的渐变角度变量
@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.glow {
  background: linear-gradient(var(--angle), #8b5cf6, #ec4899);
  transition: --angle 0.6s;  /* 现在可以过渡了 */
}
.glow:hover { --angle: 180deg; }

能力边界:媒体查询条件里不能用变量

var(--bp) 不能用在 @media 的条件表达式里——媒体查询在解析时求值,而变量是运行时按元素解析的,两者机制冲突。想做「响应式断点令牌」只能用容器查询或 CSS 预处理器的编译期变量替代。这不是 bug 而是设计使然。

合法 vs 非法
/* 非法:变量不能出现在媒体查询条件里 */
:root { --bp: 768px; }
@media (min-width: var(--bp)) { }  /* 整条规则被丢弃 */

/* 合法:变量用在媒体查询块内部 */
@media (min-width: 768px) {
  :root { --gap-md: 24px; }  /* 断点内重定义变量是推荐用法 */
}

实战最佳实践

变量用得多不如用得稳,这几条是团队协作中最值得遵守的:

  • 命名分层:--color-*、--space-*、--radius-* 前缀分类,派生值用 calc 从基础值推导,避免魔法数字。
  • 组件定制一律走变量接口:组件样式引用 var(--btn-bg, #eee),外部通过覆盖变量换肤,不改组件内部类名。
  • JS 动态改变量用 document.documentElement.style.setProperty('--x', val),比切换类名更细粒度(如拖动调色)。
  • 变量表集中放在一个 tokens.css,配合注释标注来源与用途,禁止在业务样式里随手定义新全局变量。

从零搭建最小令牌系统

按这个顺序搭,半小时内能得到一个支持主题切换与响应式的令牌骨架:

  1. 定义基础层::root 里放颜色、间距、圆角、字号基础值,全部小写前缀命名。
  2. 派生层用 calc 与 color-mix 从基础值组合,不直接写魔法数字。
  3. 为 dark 主题建 [data-theme="dark"] 覆盖块,只覆盖需要变化的变量。
  4. 组件样式只消费 var(),不含具体色值;验证切换 data-theme 全站变色。
  5. 媒体查询/容器查询里按断点重定义部分变量;记住条件里永远不能放变量。

常见问题

CSS 变量能用在 @media 的条件里吗?

不能。@media (min-width: var(--bp)) 会导致整条规则失效,因为媒体查询在解析期求值,变量是运行时按元素解析的。变量可以写在媒体查询块内部按断点重定义,这是官方支持且推荐的用法。

var() 的 fallback 什么时候生效?

只在变量完全未定义时生效。若变量已定义但值非法(如颜色位置放了数字),属性会回退到 initial 或继承值,而不是 fallback。想要「值非法也兜底」需用 @property 注册类型或在校验层处理。

JS 怎么修改 CSS 变量实现动态换肤?

用 element.style.setProperty('--color-primary', '#8b5cf6')。设在 documentElement 上即全局生效,设在具体元素上则只影响该子树。拖动调色、实时预览等高频操作推荐这种细粒度方案。

CSS 变量继承有什么要特别注意的?

变量沿 DOM 树继承且后代可覆盖,这是组件换肤的基础,但也意味着变量作用域是「位置」而非「文件」——在深层元素上定义的变量不会影响兄弟分支。排查变量不生效时,先在 DevTools computed 面板确认该元素实际解析到的值。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件