文章

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 变量有两个致命局限:

  1. 编译时绑定——编译成 CSS 后,变量被替换为固定值,不能再修改
  2. 无法响应运行时变化——不能根据媒体查询或 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(变量存在但值无效,不会用备用值!)*/
}

💡 重要陷阱

  • 变量存在但值无效 → 使用 inheritinitial
  • 变量不存在 → 使用 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 变量实现暗黑模式?

参考答案:

  1. :root 上定义两套颜色 tokens,通过 data-theme 属性切换
  2. 核心代码:
    1
    2
    
    :root[data-theme="dark"] { --bg: #1a1a2e; --text: #e0e0e0; }
    body { background: var(--bg); color: var(--text); }
    
  3. JS 切换:document.documentElement.setAttribute('data-theme', 'dark')
  4. 配合 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。

总结与扩展

核心要点

  1. CSS 变量 = --name 定义 + var() 使用
  2. 运行时变量,区别于预处理器的编译时变量
  3. 严格遵循 CSS 层叠和继承规则
  4. var(name, fallback) 支持备用值
  5. 配合 calc() 可实现动态布局计算
  6. 主题切换的最佳技术方案

延伸学习方向

  • CSS 变量与 Houdini@property 注册类型化的自定义属性
  • CSS 变量与交互动画:GSAP 等库可驱动 CSS 变量变化
  • 主题 tokens 体系设计:三层架构(全局 → 组件 → 状态)

相关主题

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量