CSS 自定义属性(CSS 变量)
Category(分类): CSS Status: 已整理
CSS 自定义属性通常被称为 CSS 变量。它们可以集中管理颜色、尺寸、间距、动画时长和主题配置,也可以在运行时通过 JavaScript 动态更新。
CSS 自定义属性不是 Sass 或 Less 那种编译阶段变量,而是浏览器运行时参与层叠、继承和计算的 CSS 属性。
一、什么是 CSS 自定义属性?
自定义属性名必须以两个连字符 -- 开头:
:root {
--main-color: hsl(240 10% 3.9%);
--secondary-color: hsl(0 0% 98%);
}
body {
background-color: var(--main-color);
color: var(--secondary-color);
}
在上例中,--main-color 和 --secondary-color 是自定义属性,通过 var() 函数读取。
有效的自定义属性名可以包含字母、数字、连字符和下划线等字符,例如:
:root {
--example-name: red;
--example--name: blue;
}
自定义属性名区分大小写:
:root {
--header-color: #333;
--Header-Color: #666;
}
上面是两个不同的自定义属性。
自定义属性和 Sass 变量的区别
// Sass 变量:构建阶段被替换
$main-color: #06c;
// CSS 自定义属性:保留在输出 CSS 中,运行时计算
:root {
--main-color: #06c;
}
Sass 变量在编译后通常不会保留;CSS 自定义属性会保留在浏览器中,因此可以:
- 根据媒体查询改变值;
- 根据组件作用域覆盖值;
- 通过 JavaScript 动态修改;
- 参与运行时计算和动画。
二、为什么经常在 :root 中定义变量?
:root 是一个伪类,在 HTML 文档中通常匹配根元素 <html>:
:root {
--global-bg-color: hsl(240 10% 3.9%);
--global-text-color: hsl(0 0% 98%);
--header-height: 4rem;
--content-padding: 1rem 1.5rem;
--transition-duration: 0.35s;
}
由于自定义属性默认具有继承性,根元素上的变量通常可以被后代元素读取,因此适合放置全局设计令牌。
但“全局变量”只是便于理解的说法,它并不是编程语言中的全局变量:
- 后代元素可以覆盖它;
- 变量只沿继承链传递;
- 独立的元素树无法读取另一个元素的局部变量;
- Shadow DOM、iframe 等边界需要单独考虑;
!important和层叠规则仍然会影响最终值。
三、局部定义和覆盖
自定义属性可以定义在任意选择器中:
.section {
--section-bg-color: hsl(200 15% 40%);
background-color: var(--section-bg-color);
}
--section-bg-color 通常可以被 .section 及其后代读取,其他不在该继承范围内的元素不能直接读取它。
局部主题示例
<div class="container">
<p>默认主题文本</p>
<button class="button">局部主题按钮</button>
</div>
<button class="button">外部按钮</button>
:root {
--text-color: #111827;
}
.container {
--text-color: #fff;
padding: 1rem;
background: #1d4ed8;
}
.button {
color: var(--text-color);
}
只有 .container 内的按钮会继承白色 --text-color。外部按钮仍然使用 :root 中的深色值。
四、继承、层叠和计算
自定义属性的最终值需要结合 CSS 层叠和继承理解。浏览器会针对每个元素计算变量值,而不是在整个文档中选择一个统一的“最高优先级变量”。
<p>根元素颜色</p>
<div>div 颜色</div>
<div id="alert">警告颜色</div>
:root {
--color: blue;
}
div {
--color: green;
}
#alert {
--color: red;
}
* {
color: var(--color);
}
结果通常是:
<p>从根元素继承blue;- 普通
<div>使用自身的green; #alert使用自身层叠后的red。
自定义属性本身也遵循层叠规则:如果同一个元素上有多条匹配的声明,会根据来源、重要性、层叠层、优先级和声明顺序决定最终值。
五、var() 函数
var() 用于读取自定义属性:
.link {
color: var(--main-color);
text-decoration-color: var(--secondary-color);
}
使用回退值
var() 的第二个参数是回退值:
.card {
color: var(--text-color, #333);
}
当 --text-color 未定义、无效或形成循环时,浏览器可以使用 #333 作为回退值。
回退值中的逗号和空格会被视为回退内容的一部分:
:root {
--font-stack: "Roboto", "Helvetica", sans-serif;
--card-padding: 10px 15px 20px;
}
.text {
font-family: var(--font-stack);
padding: var(--card-padding);
}
如果变量存在,但替换后的结果不符合目标属性语法,目标声明仍可能在计算值阶段失效,不能把回退值理解为对所有错误值的自动修复。
在变量定义中引用变量
:root {
--primary-color: red;
--logo-text-color: var(--primary-color);
}
如果自定义属性互相引用形成循环,相关变量会变成无效值:
:root {
--a: var(--b);
--b: var(--a);
}
不能把变量用作属性名
.foo {
--side: margin-top;
/* 无效:var() 不能替换属性名 */
var(--side): 20px;
}
自定义属性也不能直接用作选择器名称、媒体查询名称或任意 CSS 语法结构的替代品。它们主要用于属性值和部分允许计算值的场景。
六、自定义属性值的类型和单位
默认情况下,自定义属性保存的是 CSS token 序列,浏览器不会像 Sass 那样提前把它识别成一个固定类型。
数字和单位
数字和单位不能直接拼接:
.foo {
--gap: 20;
/* 无效或无法按预期解析 */
margin-top: var(--gap)px;
}
应使用 calc():
.foo {
--gap: 20;
margin-top: calc(var(--gap) * 1px);
}
也可以直接把单位放在变量值中:
.foo {
--gap: 20px;
margin-top: var(--gap);
}
字符串值
如果变量值带引号,它通常是字符串 token,不能直接当作长度使用:
/* 无效:字符串不是长度 */
.foo {
--size: '20px';
font-size: var(--size);
}
/* 有效 */
.bar {
--size: 20px;
font-size: var(--size);
}
在 content 等允许字符串的属性中,可以组合字符串和变量:
:root {
--label: "external link";
}
.link::after {
content: " [" var(--label) "]";
}
这不是通用的字符串拼接,而是变量替换后由目标 CSS 属性重新解析。
保存给 JavaScript 的数据
自定义属性可以保存浏览器不理解的 token,JavaScript 可以读取这些 token:
:root {
--js-message: if(x > 5) this.width = 10;
}
const message = getComputedStyle(document.documentElement)
.getPropertyValue('--js-message')
.trim()
console.log(message)
这些内容不会被 CSS 执行,也不会自动变成 JavaScript。它只是 CSS 与 JavaScript 之间传递字符串数据的一种方式,实际项目应避免把不可信内容当作代码执行。
七、响应式布局中的 CSS 变量
CSS 自定义属性可以在媒体查询中重新定义:
:root {
--primary-color: #7f583f;
--secondary-color: #f7efd2;
--content-padding: 1rem;
}
.page-link {
color: var(--primary-color);
text-decoration-color: var(--secondary-color);
padding-inline: var(--content-padding);
}
@media (min-width: 768px) {
:root {
--primary-color: #f7efd2;
--secondary-color: #7f583f;
--content-padding: 2rem;
}
}
这样可以集中改变主题值,而不用重复修改多个选择器。
也可以在组件局部响应式覆盖:
.card {
--card-padding: 1rem;
padding: var(--card-padding);
}
@media (min-width: 768px) {
.card {
--card-padding: 1.5rem;
}
}
八、兼容性处理
现代主流浏览器已经广泛支持 CSS 自定义属性。需要支持不支持自定义属性的旧浏览器时,可以提供普通 CSS 回退:
.link {
color: #7f583f;
color: var(--primary-color);
}
不支持 var() 的浏览器会忽略第二条声明,继续使用第一条。
也可以使用 @supports 检测:
@supports (--custom-property: 0) {
.box {
color: var(--primary-color);
}
}
@supports not (--custom-property: 0) {
.box {
color: #7f583f;
}
}
@supports 只能检测浏览器是否理解某种语法,不能保证具体变量值在所有场景下都符合目标属性要求。
九、使用 JavaScript 动态更新 CSS 变量
1. 检测支持情况
const isSupported =
typeof CSS !== 'undefined' &&
CSS.supports('--custom-property: 0')
if (isSupported) {
console.log('支持 CSS 自定义属性')
} else {
console.log('不支持 CSS 自定义属性')
}
2. 设置、读取和删除变量
主题变量通常设置在根元素上:
const root = document.documentElement
// 设置变量
root.style.setProperty('--primary-color', '#7f583f')
// 读取根元素上的内联变量
const inlineValue = root.style
.getPropertyValue('--primary-color')
.trim()
// 删除变量
root.style.removeProperty('--primary-color')
如果要读取元素最终继承或计算得到的值,应使用 getComputedStyle():
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--primary-color')
.trim()
element.style.getPropertyValue() 只读取该元素的内联样式,不会返回来自样式表或父元素继承的值。
3. 使用鼠标坐标驱动 CSS
const root = document.documentElement
document.addEventListener('pointermove', (event) => {
root.style.setProperty('--mouse-x', `${event.clientX}px`)
root.style.setProperty('--mouse-y', `${event.clientY}px`)
})
对应 CSS:
.pointer-effect {
transform: translate(
var(--mouse-x, 0px),
var(--mouse-y, 0px)
);
}
高频事件中应注意性能,必要时使用 requestAnimationFrame 节流,避免每次指针移动都触发大量样式计算。
十、使用 @property 注册类型
默认自定义属性通常按不可插值的 token 处理。如果需要让自定义属性平滑动画,可以使用 @property 注册语法、继承方式和初始值:
@property --progress {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
.progress {
--progress: 0;
width: calc(var(--progress) * 1%);
animation: fill 1s ease-out forwards;
}
@keyframes fill {
to {
--progress: 100;
}
}
@property 的支持范围和语法限制需要根据目标浏览器测试。对于不支持的环境,应准备静态样式或 JavaScript 回退。
十一、设计令牌和主题实践
在大型项目中,可以把自定义属性分成基础令牌、语义令牌和组件令牌:
:root {
/* 基础令牌 */
--blue-600: #2563eb;
--gray-100: #f3f4f6;
--gray-900: #111827;
/* 语义令牌 */
--color-primary: var(--blue-600);
--color-surface: #fff;
--color-text: var(--gray-900);
}
[data-theme='dark'] {
--color-surface: #111827;
--color-text: var(--gray-100);
}
.button {
color: var(--color-surface);
background: var(--color-primary);
}
这样切换主题时只需要修改语义令牌,而不必修改所有组件选择器。
十二、常见误区
误区一:CSS 自定义属性就是 Sass 变量
不是。Sass 变量在构建阶段替换,CSS 自定义属性在浏览器运行时参与层叠、继承和计算。
误区二:定义在 :root 就永远不能覆盖
不是。后代元素可以重新定义同名变量,!important 和层叠规则也会影响结果。
误区三:变量可以替换任何 CSS 内容
不是。变量主要用于允许自定义属性替换的值位置,不能直接替换属性名、选择器或任意语法结构。
误区四:变量值一定有明确类型
默认自定义属性保存的是 token 序列。需要类型检查和动画插值时,可以考虑 @property。
误区五:动态更新变量不会影响性能
修改根元素上的变量可能导致大量后代重新计算样式。应尽量缩小变量作用域,并避免在高频事件中无节制地修改全局变量。
十三、图片和参考资料
CSS 自定义属性可以配合主题系统、响应式布局和 JavaScript 使用。原文中的 CSS 变量示意图已保存到本地:

参考资料:
- MDN:使用 CSS 自定义属性
- MDN:
var() - MDN:
@property - CSS Cascading Variables Module Level 1
- 理解和使用 CSS 自定义属性
- 阮一峰:CSS 变量教程
- CSS 数学函数
总结
- 使用
--name定义 CSS 自定义属性,使用var(--name)读取; :root适合定义全局设计令牌,但自定义属性仍然遵循继承和层叠;- 局部变量只对自身及其后代可用,不能被无关元素读取;
var()的第二个参数是回退值,但不能修复所有替换后的无效结果;- 自定义属性保存的是 CSS token 序列,不等同于普通字符串或 Sass 变量;
- 数字和单位应使用带单位变量或
calc()组合; @supports可以用于兼容性检测,JavaScript 可以通过 CSSOM 动态更新变量;- 高频更新变量时要注意样式计算成本;
- 需要类型约束和连续动画时,可以使用
@property; - 使用设计令牌组织颜色、尺寸和主题,可以减少重复样式并提高维护性。