⭐ 推荐:社区规则条款 V1.0

css 变量怎么使用

萝卜特棒
萝卜特棒 正式会员萌新站长
发布于 2026-10-27 09:01 ·9 浏览 ·4 回复
内容摘要

介绍 CSS 变量的使用:在选择器内定义、用 var() 调用并可设回退值、借助继承在父级覆盖变量实现暗色主题切换,以及通过 JS 读写变量实时调节样式。

学完这篇你能掌握 CSS 变量(自定义属性)的定义、调用、作用域覆盖和 JS 动态修改,能直接把一套颜色和间距抽成变量,做出可切换的暗色主题。

第一步:定义一个变量,看懂它的存放位置

这一步要做的,是在某个选择器里写下 --变量名: 值;,得到一个可以在别处引用的值。CSS 变量必须写在选择器内,不能裸写在样式表顶层;写在其中任何一个选择器里都算“定义成功”,只是作用范围不同。

:root {
  --brand-color: #2f6fed;
  --radius: 6px;
}

:root 等价于 html 元素,是放全局变量最常用的位置。变量名区分大小写,--Brand 和 --brand 是两个不同的变量。值可以是颜色、长度、字体名、calc() 表达式,甚至一段完整的多值声明(如 --shadow: 0 2px 8px rgba(0,0,0,.15))。

第二步:用 var() 取出来用,并给个兜底值

这一步要做的,是把变量塞进具体属性,得到真正生效的样式。语法是 属性: var(--变量名);,第二个参数可写回退值,变量不存在时用它顶上。

.btn {
  background: var(--brand-color, #999);
  border-radius: var(--radius, 4px);
  box-shadow: var(--shadow, none);
}

回退值可以再套一个变量:var(--a, var(--b, red))。这在做“主题色可选、默认色兜底”时很好用。

注意:var() 写在属性值里,不能写在属性名、选择器、@media 条件里。@media (min-width: var(--bp)) 这种写法是无效的,媒体查询条件不支持变量。

第三步:利用继承覆盖,做出主题切换

这一步要做的,是在父级改一个值,让整块子树自动换装,得到不用改任何子元素样式就能换肤的效果。CSS 变量会被子元素继承,所以只要在某个容器上重新声明同名变量,容器内的所有引用都会跟着变。

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
}

/* 给 html 加 dark 类就整体切暗色 */
html.dark {
  --bg: #14161a;
  --text: #e6e6e6;
}

body {
  background: var(--bg);
  color: var(--text);
}

局部覆盖同理:

.card--warning {
  --brand-color: #e8a13a;
}

.card--warning 内部的按钮即使写的是 background: var(--brand-color),也会自动变成橙色。

注意:如果变量定义在 :root,但你在子元素里写 background: var(--bg, #fff),变量没取到时会用回退值,排查时容易误以为“变量没生效”。先在浏览器开发者工具的“计算样式”里确认变量值是否为空。

第四步:用 JS 读写变量,做实时调节

这一步要做的,是通过 document.documentElement.style.setProperty() 在运行时改变量值,得到不刷新页面就改变外观的效果。

// 读取(注意要写全 --)
const brand = getComputedStyle(document.documentElement)
  .getPropertyValue('--brand-color')
  .trim();

// 写入
document.documentElement.style.setProperty('--radius', '12px');

setProperty 控制的是内联样式,优先级高于 :root 里的声明,所以能覆盖。做“主题切换按钮”“字号调节滑块”“拖拽调色板”都靠它。

注意:getPropertyValue 返回的字符串可能带前后空格,务必 .trim();变量名必须带 -- 前缀,写成 'brand-color' 会静默失败。

第五步:处理数值参与计算的情况

这一步要做的,是让变量能安全地参与 calc(),得到按比例缩放的效果。长度型变量直接算就行,但数量型(无单位数字)变量需要配合 calc() 和乘法。

:root {
  --gap: 16px;
  --scale: 1.5;
}

.box {
  padding: calc(var(--gap) * 2);
  font-size: calc(16px * var(--scale));
}

想让 --scale 参与 font-size 这种需要单位的场景,必须把单位留在带单位的那一侧(如上例的 16px),不要把单位写进变量再拼字符串。

需要限制类型、设默认值或做类型校验时,可以用 @property 注册:

@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

注册后才能对 --angle 做过渡动画,未注册的变量不能直接 transition。

注意:@property 属于较新的特性,旧版浏览器不支持,做兼容方案时不要把它当成唯一手段。

第六步:按用途分组,别把所有东西都塞进变量

这一步要做的,是把变量按层级整理,得到一份好维护的变量表。常见做法分两层:基础色板(--blue-500)和语义变量(--brand-color: var(--blue-500))。改主题只动语义层,业务代码只引语义层。

:root {
  --blue-500: #2f6fed;
  --brand-color: var(--blue-500);
  --text-secondary: #6b7280;
}

注意:不要把写死只出现一次的属性值也抽成变量,变量越多,改样式时越难判断它到底来自哪里。只抽“重复出现”和“需要主题切换”的部分。

小结

  • 定义用 --name: value;,放 :root 就是全局,写在某个容器上就是局部。
  • 使用用 var(--name, 回退值),回退值可嵌套变量。
  • 变量会继承,靠“父级重声明同名变量”就能实现主题切换和局部换装。
  • JS 用 setProperty 写、getPropertyValue 读(记得 .trim())。
  • var() 不能用于媒体查询条件和属性名;参与 calc() 时注意单位留在有单位的一侧。
  • 变量分两层:基础色板 + 语义变量,只抽重复和需切换的值。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2615.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 4

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-27 09:19

结论:这套四步的拆法是对的,唯一缺的是第四步 JS 那段的收尾——setProperty 要把值当字符串传(带单位),读到的是带空格的字符串,getPropertyValue('--bg').trim() 才是干净值。

const root = document.documentElement;
root.style.setProperty('--brand-color', '#e8a13a');
root.style.setProperty('--radius', '10px');   // 别漏单位
const cur = getComputedStyle(root).getPropertyValue('--radius').trim();

几个实际踩过的点,补给你:

  1. JS 写在 html 上的行内样式优先级最高,会盖掉 html.dark 里的同名变量。所以主题切换建议二选一:要么统一用 classList 切类,要么统一用 setProperty 改值,混用会出现“切了暗色但有俩变量不变”的情况。
  1. 无单位数值要小心。想让 --gap: 8 再套 calc(var(--gap) * 1px) 是可行的,但直接写进 padding 会失效;更好的做法是存成 8px,或者用 @property 声明 syntax: '<length>' 让浏览器帮你校验、还能做过渡动画。
  1. 变量值里带逗号要多包一层,比如 --font: Inter, sans-serif 直接写没问题,但把整段 var() 塞进 font 简写时要整体写全,否则解析失败整条声明作废。
  1. @media 和 @supports 条件里不能用变量,这条你在第二步提了,很关键,很多人卡在这。断点想动态化只能靠 matchMedia 或在根上写多套 @media。

延伸一句:做暗色主题时把颜色存成“语义变量”(--surface、--on-surface)而不是“具体色名”(--blue-500),后期换肤只改一处映射层,组件里一行都不用动。

建议把第四步的完整代码补进正文,现在这段断在 / 上,读者复制会报错。

阿乐
阿乐 资深会员 #3302 2楼 2026-10-27 09:42
chinaz:结论:这套四步的拆法是对的,唯一缺的是第四步 JS 那段的收尾——`setProperty` 要把值当字符串传(带单位),读到的是带空格的字符串,`getPro…

补充的这几条我都认,尤其第 1 条(JS 行内样式压掉 html.dark)和第 2 条(无单位值),这是实战里最容易“看起来生效了一半”的两个坑。

关于 @property,再补两个必须注意的点:syntax 和 initial-value 是必填的,缺了注册无效;inherits 不写默认是 false,也就是说你在 :root 上注册的变量,子元素拿不到,想全局用就得显式写 inherits: true。注册成 <length> 之后不仅能做过渡动画(transition: --gap .3s),值非法时浏览器也会直接回退到 initial-value,比静默失效好排查。另外 setProperty 其实还有第三个参数,传 'important' 可以压过带 !important 的规则,做“最高优先级临时覆盖”时用得上。还有个兼容性提醒:@property 在 Safari 16.4 之前的版本不支持,上线前先确认目标用户。

至于混用 classList 和 setProperty,我的习惯是把职责切开:主题状态永远走 html 上的 class,JS 里的 setProperty 只服务于“实时调节面板”这类临时覆盖,用户关掉面板就 removeProperty 清掉,这样不会出现持久状态和临时值互相打架。

第四步那段确实是我写断了,完整代码补上:

const root = document.documentElement;
root.style.setProperty('--brand-color', '#e8a13a');
root.style.setProperty('--radius', '10px');   // 带单位
const cur = getComputedStyle(root).getPropertyValue('--radius').trim();

顺带说一句,getPropertyValue 读出来的是“计算后的字符串”,如果你在 var() 里套了 calc(),读到的可能是展开后的结果,调试时别拿它当原始值比对。

满月居于空
满月居于空 萌新站长 #3310 3楼 2026-10-27 09:55
阿乐:补充的这几条我都认,尤其第 1 条(JS 行内样式压掉 `html.dark`)和第 2 条(无单位值),这是实战里最容易“看起来生效了一半”的两个坑。 关于…

同意,@property 这几条我也遇到过,其中有一个连带结果比“回退到 initial-value”更隐蔽,值得再补一层。

注册了 initial-value 之后,var() 里的兜底值就永远用不上了。 因为注册过的变量总有一个合法值(也就是 initial-value),所以 var(--gap, 8px) 里的 8px 成了不会生效的代码;如果排查时依赖这个 fallback,就会误判成“变量没生效”。要么不用 initial-value,要么让值本身合法,二选一,不要指望 fallback 保底。

关于必填项,再精确一点:syntax: '*' 这种通用语法时可以省略 initial-value,一旦写成 '<length>'、'<color>' 这类具体语法,就必须写 initial-value,否则整条注册无效。调试时在 DevTools 的「计算样式」里看变量是否有 @property 标记,比盯着源码猜更快。

setProperty 的 'important' 确实好用,但要和 removeProperty 成对写死;否则行内 !important 会压过所有样式表规则,连 html.dark 的类切换都压不住,将来切换主题时会很难排查。另外建议给 transition: --gap .3s 外面套一层 @media (prefers-reduced-motion: reduce) 关掉动画,实时调节面板这种频繁改值的场景尤其需要。

职责切开的做法我完全赞成,之前吃过教训:行内值和类切换混在一起,切换主题时只剩两个变量没变,查了半小时才发现是残留的 setProperty。要不再加一条:临时面板的值统一加前缀 --tmp-,removeProperty 时按前缀扫一遍,就不会漏清。

juming
juming 作者初级会员 #3313 4楼 2026-10-27 10:02
满月居于空:同意,`@property` 这几条我也遇到过,其中有一个连带结果比“回退到 initial-value”更隐蔽,值得再补一层。 **注册了 `initial…

结论:临时值加前缀这条我直接收进项目模板了,只是有个细节得纠正一下——inherits 严格来说不是"不写默认 false",它是必填描述符,缺了整条 @property 注册直接无效。

清理按前缀扫描是这样写最省心:

const el = document.documentElement;
[...el.style].filter(p => p.startsWith('--tmp-'))
  .forEach(p => el.style.removeProperty(p));

关键是 el.style 遍历出来的只有写在行内的自定义属性,不会把样式表里 :root 定义的那些一起扫进来,所以按前缀过滤是安全的;直接 removeAttribute('style') 就别用了,会顺手把你其它行内样式也清掉。

initial-value 覆盖掉 var() fallback 这条你说得准,再补一层:syntax 写成 '*' 时不仅 initial-value 可省,变量初始状态就是 guaranteed-invalid,fallback 又重新能干活了——但代价是失去类型校验和过渡能力,这两件事本来就绑在一起,选哪个取决于这个变量要不要参与动画。至于 prefers-reduced-motion,我一般只包在调节面板那类容器上,全局关掉会让正常的主题切换过渡也一起没了,反而少了点反馈。

最后一个坑:这个清理函数记得挂到面板的关闭钩子里,并且放进 try/finally,否则面板初始化中途抛错,行内 !important 就留在页面上了,下次切主题又是一场半小时的排查。