最后更新:2026-03-20

CSS 自定义属性(也叫 CSS 变量)支持已经很久了,但我发现不少人还在用 SCSS 变量或者把颜色值写死在每个地方。

其实 CSS 原生变量比 SCSS 变量能做更多事,最典型的就是运行时动态修改——这是预处理器变量做不到的。

基本语法

定义变量用 -- 开头,使用时用 var() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
:root {
--color-primary: #3b82f6;
--color-text: #1f2937;
--spacing-base: 8px;
--border-radius: 6px;
}

.button {
background-color: var(--color-primary);
color: white;
padding: calc(var(--spacing-base) * 1.5) calc(var(--spacing-base) * 3);
border-radius: var(--border-radius);
}

var() 还支持默认值,当变量未定义时回退:

1
2
3
4
5
6
.card {
/* 如果 --card-bg 没定义,就用 white */
background: var(--card-bg, white);
/* 可以嵌套,默认值也可以是另一个变量 */
color: var(--card-text, var(--color-text, #333));
}

CSS 变量和 SCSS 变量的区别

这是个容易混淆的点,两者看起来都是「变量」,但性质不同:

SCSS 变量:编译时替换,最终输出的 CSS 里没有变量,只有固定值。

1
2
3
4
$primary: #3b82f6;

// 编译后变成:background-color: #3b82f6;
.button { background-color: $primary; }

CSS 变量:运行时存在,可以被 JavaScript 读写,可以在某个元素的作用域内覆盖。

1
2
3
4
5
6
7
8
:root { --primary: #3b82f6; }

/* 可以在某个元素下覆盖 */
.dark-section {
--primary: #93c5fd;
}

.button { background-color: var(--primary); }

.dark-section 里面的 .button 会用 #93c5fd,外面的用 #3b82f6。这种作用域覆盖是 SCSS 做不到的。

实现主题切换

这是 CSS 变量最常见的应用场景,也是我项目里真正在用的:

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
/* 默认(亮色)主题 */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--color-brand: #3b82f6;
}

/* 暗色主题,覆盖同名变量 */
[data-theme='dark'] {
--bg-primary: #111827;
--bg-secondary: #1f2937;
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--border-color: #374151;
--color-brand: #60a5fa;
}

/* 组件只用变量,不关心当前是哪个主题 */
.card {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}

用 JavaScript 切换主题只需要一行:

1
2
3
4
5
6
7
// 切换到暗色
document.documentElement.setAttribute('data-theme', 'dark')

// 切回亮色
document.documentElement.removeAttribute('data-theme')
// 或者
document.documentElement.setAttribute('data-theme', 'light')

配合 localStorage 持久化:

1
2
3
4
5
6
7
8
9
10
11
12
const THEME_KEY = 'preferred-theme'

function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem(THEME_KEY, theme)
}

// 页面加载时恢复上次的主题
const savedTheme = localStorage.getItem(THEME_KEY)
if (savedTheme) {
setTheme(savedTheme)
}

用 JavaScript 读写 CSS 变量

1
2
3
4
5
6
7
8
9
10
// 读取
const primary = getComputedStyle(document.documentElement)
.getPropertyValue('--color-brand')
.trim()

// 在某个元素上设置(只影响该元素及其子元素)
element.style.setProperty('--color-brand', '#ef4444')

// 移除
element.style.removeProperty('--color-brand')

这个能力很有用,比如拖拽调整颜色、用户自定义主题色等场景:

1
2
3
4
// 用户选了颜色,实时更新
colorPicker.addEventListener('input', (e) => {
document.documentElement.style.setProperty('--color-brand', e.target.value)
})

响应式设计里的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:root {
--font-size-base: 16px;
--container-padding: 24px;
--columns: 3;
}

@media (max-width: 768px) {
:root {
--font-size-base: 14px;
--container-padding: 16px;
--columns: 1;
}
}

.container {
padding: var(--container-padding);
font-size: var(--font-size-base);
}

.grid {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
}

在媒体查询里覆盖变量,只需要改一处,所有用到这个变量的地方都会跟着变。

组件级变量

CSS 变量的作用域可以限定在特定组件:

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: #3b82f6;
--btn-color: white;
--btn-padding: 8px 16px;

background: var(--btn-bg);
color: var(--btn-color);
padding: var(--btn-padding);
}

/* 变体:覆盖组件内部变量 */
.btn-danger {
--btn-bg: #ef4444;
}

.btn-ghost {
--btn-bg: transparent;
--btn-color: #3b82f6;
}

/* 大尺寸:只改 padding */
.btn-large {
--btn-padding: 12px 24px;
}

这种写法比写很多条选择器覆盖属性要清晰很多,修改成本也低。

和 SCSS 一起用

CSS 变量和 SCSS 并不冲突,可以配合用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 用 SCSS 定义调色板(编译时常量)
$colors: (
'blue-500': #3b82f6,
'blue-400': #60a5fa,
'gray-900': #111827,
);

// 生成 CSS 变量
:root {
@each $name, $value in $colors {
--color-#{$name}: #{$value};
}
}

SCSS 负责组织和生成,CSS 变量负责运行时动态能力,各司其职。


CSS 变量的浏览器支持已经很好了,IE 不支持但你大概也不需要兼容 IE 了。新项目建议直接用起来,配合主题切换效果很好,改起来也方便。