CSS变量与自定义属性深入解析
CSS变量与自定义属性深入解析
一句话概括
CSS 变量(Custom Properties,自定义属性)允许我们在 CSS 中定义可复用的值,并通过 var() 函数在任意选择器中使用,配合 JavaScript 和媒体查询可以实现运行时的样式复用、主题切换和动态计算——它弥补了 CSS 没有”变量”的先天不足,是现代 CSS 工程化的基石之一。
背景与意义
CSS 的”硬编码”困境
在 CSS 变量出现之前,CSS 中任何重复的值都是硬编码的:
1
2
3
4
5
6
7
8
/* ❌ 硬编码的痛苦 */
.card { background: #42b883; border-color: #42b883; }
.btn { background: #42b883; color: white; }
.badge { background: #42b883; }
.header { border-bottom: 2px solid #42b883; }
/* 如果品牌色从 #42b883 改为 #3aa876 …… 要改 4 个地方 */
/* 而且"#42b883"没有任何语义——哪个是主色?哪个是辅助色? */
Sass/Less 预处理器的变量解决了部分问题:
1
2
3
// Sass 变量:编译时替换
$primary: #42b883;
.card { background: $primary; }
但 Sass 变量有两个致命局限:
- 编译时绑定——编译成 CSS 后,变量被替换为固定值,不能再修改
- 无法响应运行时变化——不能根据媒体查询或 JS 动态修改变量
CSS 变量真正突破了这些限制——它是运行时变量,浏览器引擎直接管理。
面试高频信号
CSS 变量面试题在 2025-2026 年趋于高频,常见角度:
- var() 的备用值机制
- CSS 变量与预处理器的对比
- 基于 CSS 变量的主题切换实现
- CSS 变量在 animation 中的动态值
概念与定义
基本语法
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 定义变量:写在一组选择器的声明块中 */
:root { /* :root 等于 html,但优先级更高 */
--primary: #42b883;
--radius: 8px;
--font-base: 16px;
}
/* 使用变量 */
.card {
background: var(--primary);
border-radius: var(--radius);
font-size: var(--font-base);
}
变量命名规则
- 以
--开头(两个减号) - 大小写敏感:
--Primary和--primary是两个不同变量 - 值可以是任何合法的 CSS 值:颜色、长度、数字、字符串、甚至整个
background简写
1
2
3
4
5
6
7
8
:root {
--spacing: 16px;
--gutter: calc(var(--spacing) * 2); /* 变量可以使用其他变量 */
--shadow: 0 2px 8px rgba(0,0,0,0.12);
--font-stack: "Inter", system-ui, sans-serif;
--border: 1px solid var(--primary); /* 变量组合 */
--empty: ; /* 空值也是合法变量 */
}
var() 函数的备用值机制
1
2
3
4
5
6
7
8
9
10
11
/* var(变量名, 备用值) — 如果变量未定义,使用备用值 */
.card {
background: var(--card-bg, #f5f5f5);
/* --card-bg 未定义 → 使用 #f5f5f5 */
}
/* 嵌套备用 */
.card {
border-color: var(--border-color, var(--primary, #333));
/* 先查 --border-color → 未定义则查 --primary → 仍未定义则 #333 */
}
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<style>
:root {
--bg: white;
--text: #333;
--accent: #42b883;
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui;
transition: background 0.3s, color 0.3s;
}
button {
background: var(--accent);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
}
.card {
background: var(--bg);
border: 1px solid var(--accent);
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
</style>
<div class="card">
<h2>CSS 变量演示</h2>
<p>背景和文字颜色由 :root 变量控制</p>
<button>主色按钮</button>
</div>
通过 JS 动态修改变量的值:
1
2
3
4
// 一键切换主题
document.documentElement.style.setProperty('--bg', '#1a1a2e');
document.documentElement.style.setProperty('--text', '#e0e0e0');
document.documentElement.style.setProperty('--accent', '#e94560');
核心知识点拆解
知识点 1:CSS 变量的继承规则
CSS 变量严格按照 CSS 的层叠和继承规则传播:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:root {
--color: blue; /* 全局默认 */
}
.container {
--color: red; /* .container 内部覆盖为红色 */
}
.container .child {
color: var(--color); /* 继承 .container 的 --color = red */
}
.sibling {
color: var(--color); /* 继承 :root 的 --color = blue */
}
这比 Sass 变量的作用域灵活得多——不仅局限于嵌套的语法结构,而是遵循真正的 CSS 选择器优先级。
知识点 2:CSS 变量与预处理器变量的对比
| 维度 | CSS 变量 | Sass/Less 变量 |
|---|---|---|
| 处理时机 | 运行时(浏览器引擎) | 编译时(构建阶段) |
| 作用域 | 遵循 CSS 选择器继承规则 | 遵循 Sass 代码嵌套作用域 |
| 动态修改 | ✅ JS 可任意修改 | ❌ 编译后不可变 |
| 媒体查询内修改 | ✅ 可 | ❌ 不可 |
| animation 中使用 | ✅ 可在运行时变化 | ❌ 编译时已固定 |
| calc() 配合 | ✅ 可参与运行时计算 | ❌ 编译时已替换 |
| 跨 CSS 文件共享 | ✅ | ❌ 需 @import |
| 浏览器兼容 | IE11 不支持 | 无兼容问题(编译后为纯 CSS) |
🎯 面试结论:两者不是替代关系,是互补关系——Sass 变量用于构建时的主题 tokens 生成,CSS 变量用于运行时的动态交互。
知识点 3:CSS 变量与 calc() 的动态计算
CSS 变量可以参与 calc() 运算,这是实现动态响应式布局的关键能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
:root {
--grid-columns: 3;
--gap: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--gap);
}
/* 响应式:媒体查询内修改 --grid-columns */
@media (max-width: 768px) {
:root { --grid-columns: 2; }
}
@media (max-width: 480px) {
:root { --grid-columns: 1; }
}
1
2
3
4
5
6
7
/* 动态字号:字号 = 基准 + 视口增量 */
:root {
--font-scale: 0; /* JS 可修改这个值实现缩放 */
}
body {
font-size: calc(16px + var(--font-scale) * 2px);
}
知识点 4:无效变量值的行为
1
2
3
4
5
6
7
8
9
10
:root {
--invalid: 16px;
}
.card {
/* 以下两句效果不同! */
color: var(--invalid); /* → color: 16px(无效值,浏览器忽略该声明)*/
color: var(--undefined, red); /* → color: red(备用值生效)*/
color: var(--invalid, red); /* → color: inherit(变量存在但值无效,不会用备用值!)*/
}
💡 重要陷阱:
- 变量存在但值无效 → 使用
inherit或initial - 变量不存在 → 使用
var(, 备用值)的备用值 - 所以不能依赖备用值来”兜底”无效变量
实战案例
案例一:一键明暗主题切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 定义两套颜色 tokens */
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a2e;
--text-secondary: #666;
--accent: #42b883;
--border: #e0e0e0;
--shadow: 0 2px 12px rgba(0,0,0,0.08);
}
:root[data-theme="dark"] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent: #e94560;
--border: #333;
--shadow: 0 2px 12px rgba(0,0,0,0.3);
}
/* 只用一种写法适配双主题 */
.card {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
1
2
3
4
5
6
// 切换主题
function toggleTheme() {
const root = document.documentElement;
const current = root.getAttribute('data-theme');
root.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
}
案例二:CSS 变量驱动的动画
1
2
3
4
5
6
7
8
9
10
11
12
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(var(--distance, 100px)); }
}
.box {
--distance: 100px;
animation: move 0.5s ease-out;
}
/* JS 控制移动距离 */
box.style.setProperty('--distance', `${Math.random() * 300}px`);
案例三:组件级的局部变量覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 按钮组件:通过变量暴露定制点 */
.btn {
--btn-bg: #42b883;
--btn-color: white;
--btn-radius: 8px;
--btn-padding: 12px 24px;
background: var(--btn-bg);
color: var(--btn-color);
border-radius: var(--btn-radius);
padding: var(--btn-padding);
border: none;
cursor: pointer;
}
/* 在特定容器中覆盖按钮变量 */
.danger-zone {
--btn-bg: #e74c3c;
--btn-color: white;
}
.checkout-section {
--btn-bg: #f39c12;
--btn-padding: 16px 48px;
}
1
2
3
4
5
6
<div class="danger-zone">
<button class="btn">删除账户</button> <!-- 红色按钮 -->
</div>
<div class="checkout-section">
<button class="btn">结算</button> <!-- 橙色按钮 + 大内边距 -->
</div>
底层原理
CSS 变量在渲染引擎中的处理流程
1
2
3
4
5
6
7
8
9
10
11
12
13
解析 HTML/CSS
↓
构建 CSSOM(CSS Object Model)
↓
识别自定义属性(--*-)—— 与普通属性走同样的解析流程
↓
计算声明值 —— 收集变量定义
↓
计算已用值 —— 遇到 var() 函数时,
在作用域链上查找变量定义,
找到则替换,未找到则使用备用值
↓
继承传播 —— 子元素继承父元素的计算后值(不是原始定义)
关键 insight:CSS 变量的”查找”是一个运行时作用域链查找,与 JS 的作用域链类似但方向相反——从当前元素开始,向上(父元素)查找。
CSS 变量与 computed style
1
2
3
4
5
6
7
8
9
10
// 读取 CSS 变量的计算值
const styles = getComputedStyle(document.documentElement);
const primary = styles.getPropertyValue('--primary').trim();
// → "#42b883"
// 设置变量
document.documentElement.style.setProperty('--primary', '#e94560');
// 删除变量
document.documentElement.style.removeProperty('--primary');
高频面试题解析
Q1:CSS 变量与 Sass 变量的区别?
参考答案:
- Sass 变量在编译时处理,CSS 变量在运行时处理
- CSS 变量支持 JS 动态修改(
setProperty),可以实现主题切换 - CSS 变量遵循 CSS 继承和层叠规则,Sass 变量遵循嵌套作用域
- CSS 变量可参与
calc()运行时计算,Sass 变量编译后已固定 - 两者互补:Sass 管理构建时 tokens,CSS 变量管理运行时交互
Q2:如何使用 CSS 变量实现暗黑模式?
参考答案:
- 在
:root上定义两套颜色 tokens,通过data-theme属性切换 - 核心代码:
1 2
:root[data-theme="dark"] { --bg: #1a1a2e; --text: #e0e0e0; } body { background: var(--bg); color: var(--text); }
- JS 切换:
document.documentElement.setAttribute('data-theme', 'dark') - 配合
prefers-color-scheme媒体查询自动应用系统主题:1 2 3
@media (prefers-color-scheme: dark) { :root { --bg: #1a1a2e; --text: #e0e0e0; } }
Q3:var() 函数中的备用值在什么情况下生效?
参考答案:
- 变量未定义(从未声明或在当前作用域不可访问)→ 备用值生效
- 变量已定义但值为无效值(例如
color: 16px)→ 浏览器忽略该声明,不会使用备用值 - 变量值为空(
--empty: ;)→ 使用空值,不会触发备用值
Q4:CSS 变量可以存储什么类型的值?
参考答案: 可以是任何合法的 CSS 值:颜色值、长度值、数字、百分比、字符串、calc() 表达式、甚至多值组合(如 10px 20px),但不能存储整个选择器或 @rule。
总结与扩展
核心要点
- CSS 变量 =
--name定义 +var()使用 - 运行时变量,区别于预处理器的编译时变量
- 严格遵循 CSS 层叠和继承规则
var(name, fallback)支持备用值- 配合
calc()可实现动态布局计算 - 主题切换的最佳技术方案
延伸学习方向
- CSS 变量与 Houdini:
@property注册类型化的自定义属性 - CSS 变量与交互动画:GSAP 等库可驱动 CSS 变量变化
- 主题 tokens 体系设计:三层架构(全局 → 组件 → 状态)
相关主题
本文由作者按照 CC BY 4.0 进行授权