CSS 自定义属性(变量)
CSS 自定义属性(变量)
CSS 自定义属性(CSS Variables,规范名 CSS Custom Properties)允许定义可复用的值,提高代码的可维护性和灵活性。它和 Sass 变量最大的不同在于:它是"活的"——运行时可读可改,还会随 DOM 级联和继承。这一点让它成为主题换肤、动态样式、设计令牌(design tokens)落地的首选工具。
一个直观的类比
Sass 变量像"写文章时的查找替换":编译时把 `$primary` 全部替换成 `#007bff`,产物里根本没有变量这回事。CSS 变量像"文档里的引用/字段":产物里始终存在这个字段,页面运行时改一下字段值,所有引用它的地方立刻更新。正因为它活到了运行时,才能配合 JavaScript、媒体查询、`:hover` 等动态改变。
为什么重要
基本语法
定义变量:
使用变量:
示例:
:root {
--primary-color: #007bff;
--font-size: 16px;
}
.button {
background-color: var(--primary-color);
font-size: var(--font-size, 14px); /* 若未定义则 14px */
}作用域和继承
全局变量:
局部变量:
继承:
代码示例
定义和使用变量(设计令牌雏形)
:root {
/* 颜色 */
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
/* 字体 */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-size-base: 16px;
--font-size-large: 1.25rem;
--font-size-small: 0.875rem;
/* 间距(用一个基准单位派生一整套) */
--spacing-unit: 8px;
--spacing-sm: var(--spacing-unit);
--spacing-md: calc(var(--spacing-unit) * 2);
--spacing-lg: calc(var(--spacing-unit) * 3);
/* 边框与阴影 */
--border-radius: 4px;
--border-width: 1px;
--border-color: #dee2e6;
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--box-shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.button {
padding: var(--spacing-md) var(--spacing-lg);
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
font-family: var(--font-family-base);
font-size: var(--font-size-base);
box-shadow: var(--box-shadow);
cursor: pointer;
transition: all 0.3s ease;
}
.button:hover { box-shadow: var(--box-shadow-lg); }
.button--secondary { background-color: var(--secondary-color); }
.button--success { background-color: var(--success-color); }
.button--danger { background-color: var(--danger-color); }主题切换(换肤)
/* 浅色主题(默认) */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* 深色主题:只改变量,其它样式不动 */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s, color 0.3s;
}
.card {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: 0 2px 4px var(--shadow-color);
color: var(--text-primary);
}
.text-secondary { color: var(--text-secondary); }// JavaScript 切换主题
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
// 初始化:优先读本地存储,其次跟随系统偏好
const saved = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', saved || (prefersDark ? 'dark' : 'light'));用 JavaScript 读写变量
const root = document.documentElement;
// 读取变量值
const primary = getComputedStyle(root).getPropertyValue('--primary-color').trim();
// 设置变量值(立即全站生效)
root.style.setProperty('--primary-color', '#e91e63');
// 交互式:跟随鼠标位置更新变量,做聚光灯/渐变效果
document.addEventListener('mousemove', (e) => {
root.style.setProperty('--mouse-x', e.clientX + 'px');
root.style.setProperty('--mouse-y', e.clientY + 'px');
});与 calc() 结合使用
:root {
--spacing-unit: 8px;
--border-width: 2px;
--font-size: 16px;
}
.card {
padding: calc(var(--spacing-unit) * 2);
margin: calc(var(--spacing-unit) * 1.5);
border: var(--border-width) solid #ddd;
font-size: calc(var(--font-size) * 1.125);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: calc(var(--spacing-unit) * 2);
}
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
gap: calc(var(--spacing-unit) * 1.5);
}
}局部覆盖:组件级变量
/* 组件内部用变量做"可配置接口",外部改一个变量就能定制 */
.badge {
--badge-bg: var(--primary-color);
--badge-fg: #fff;
background: var(--badge-bg);
color: var(--badge-fg);
padding: 2px 8px;
border-radius: 10px;
}
/* 危险徽标只需覆盖局部变量,无需重写整块样式 */
.badge--danger { --badge-bg: var(--danger-color); }
.badge--muted { --badge-bg: #e9ecef; --badge-fg: #495057; }媒体查询里改变量:一处定义,全局响应
:root { --gap: 24px; --cols: 4; }
@media (max-width: 1024px) { :root { --gap: 16px; --cols: 3; } }
@media (max-width: 768px) { :root { --gap: 12px; --cols: 2; } }
@media (max-width: 480px) { :root { --gap: 8px; --cols: 1; } }
.grid {
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
gap: var(--gap);
}
/* 所有用到 --gap / --cols 的地方都自动跟着断点变化 */@property 注册变量(可做动画/类型约束)
/* 普通 CSS 变量不能被平滑动画;注册后带类型可插值 */
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.spinner {
background: conic-gradient(#007bff var(--angle), #eee 0);
animation: rotate 1s linear infinite;
}
@keyframes rotate {
to { --angle: 360deg; } /* 因已注册类型,能平滑过渡 */
}与预处理器变量的区别
CSS 变量(`--x`):
Sass/Less 变量(`$x` / `@x`):
两者常常配合:用 Sass 做编译期批量生成,用 CSS 变量承载运行时可变的主题值。
真实案例
案例 1:一键换肤。 某 SaaS 后台需要浅色/深色/护眼三套主题。早期为每套主题打包一份 CSS(约 3 份 × 各 200KB),切换要重新加载样式、有白屏闪烁。改为一套样式 + 三组 CSS 变量后,切换只改根节点 `data-theme`,零网络请求、无闪烁,CSS 总体积下降约 60%。
案例 2:品牌可配置。 某白标(white-label)产品要给不同客户换主色。以前每个客户维护一份编译好的 Sass 变量产物。改为运行时注入 CSS 变量后,服务端只下发一段 `:root { --brand: #xxx }`,同一份 CSS 服务所有客户,新客户接入从 1 天缩短到几分钟。
案例 3:交互式渐变。 某落地页要做"跟随鼠标的聚光灯"效果。用 JS 每帧更新 `--mouse-x/--mouse-y` 两个变量,CSS 用 `radial-gradient` 引用,避免了每帧重写整段内联样式,代码更干净、性能更好。
数据与对比
| 维度 | CSS 变量 | Sass 变量 |
| --- | --- | --- |
| 生效时机 | 运行时 | 编译时 |
| JS 可读写 | 是 | 否 |
| 级联/继承 | 有 | 无 |
| 局部覆盖 | 天然支持 | 需重新赋值/作用域技巧 |
| 编译期计算 | 弱(靠 calc) | 强(循环/函数) |
| 产物体积 | 变量保留在产物 | 被替换为字面值 |
| 典型用途 | 主题、动态样式、tokens | 批量生成、编译期逻辑 |
浏览器支持:所有现代浏览器(Chrome 49+、Firefox 31+、Safari 9.1+、Edge 15+)全面支持 CSS 变量,全球覆盖率约 97%+;IE 全系不支持。`@property` 支持稍晚(Chrome 85+、Safari 16.4+、Firefox 128+)。
常见坑
最佳实践
深入:无效值与"计算时无效"(IACVT)
CSS 变量有一个反直觉但极其重要的机制:当 `var()` 引用的变量值在使用处不合法时,该属性不会回退到你以为的默认值,而是变成 `unset`——即取继承值(可继承属性)或初始值(不可继承属性)。这叫 IACVT(Invalid At Computed Value Time,计算时无效)。
:root {
--color: 20px; /* 存了一个长度值 */
}
.box {
color: red; /* 先设一个正常颜色 */
color: var(--color); /* 20px 不是合法颜色 → 该声明作废 */
/* 结果不是 red,而是继承来的颜色或初始值 currentColor 逻辑,
很多人误以为会回退到上一行的 red,其实不会 */
}关键结论: `var()` 的第二个参数(fallback)只在变量"未定义/为空"时生效,不能兜住"值非法"的情况。因此存值时就要保证类型正确,或用 `@property` 约束类型(见下)。
/* fallback 只兜"没定义",不兜"定义了但非法" */
.a { color: var(--maybe-undefined, blue); } /* --maybe-undefined 没定义 → blue,OK */
:root { --bad: 10px; }
.b { color: var(--bad, blue); } /* 定义了但非法 → 不是 blue,而是 IACVT */@property 深入:类型、动画与继承控制
`@property` 把自定义属性"升级"为带类型定义的注册属性,带来三个能力:类型校验(非法值被丢弃而非污染)、可动画(能被插值过渡)、可控制继承。
@property --brand-h {
syntax: '<number>'; /* 只接受数字 */
inherits: false; /* 不向子元素继承 */
initial-value: 210; /* 默认色相 */
}
@property --progress {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
/* 注册后,非法值会被忽略并回退到 initial-value,而不是引发 IACVT */
.ring {
background: conic-gradient(#4caf50 var(--progress), #eee 0);
transition: --progress 0.6s ease; /* 现在能平滑动画了 */
}
.ring:hover { --progress: 75%; }常见 syntax 取值:`
/* 联合类型 + 列表示例 */
@property --gap {
syntax: '<length> | auto';
inherits: true;
initial-value: 16px;
}也可以用 JS 注册(需在样式解析前执行):
CSS.registerProperty({
name: '--tilt',
syntax: '<angle>',
inherits: false,
initialValue: '0deg',
});分层设计令牌:primitive → semantic → component
成熟设计系统会把变量分三层,避免"到处直接引用原始色值"造成的强耦合。改主题时只动语义层,原始层与组件层几乎不用改。
:root {
/* 第 1 层 primitive(原始值,不带语义,只是调色板) */
--blue-500: #3b82f6;
--blue-600: #2563eb;
--gray-50: #f9fafb;
--gray-900: #111827;
--red-500: #ef4444;
/* 第 2 层 semantic(语义令牌,业务只认这层) */
--color-bg: var(--gray-50);
--color-text: var(--gray-900);
--color-accent: var(--blue-500);
--color-accent-hover: var(--blue-600);
--color-danger: var(--red-500);
/* 第 3 层 component(组件令牌,可被单组件覆盖) */
--button-bg: var(--color-accent);
--button-bg-hover: var(--color-accent-hover);
--button-fg: #fff;
}
/* 深色主题只需重定义语义层,原始层与组件层自动跟随 */
[data-theme='dark'] {
--color-bg: var(--gray-900);
--color-text: var(--gray-50);
--color-accent: var(--blue-600);
}
.button {
background: var(--button-bg);
color: var(--button-fg);
}
.button:hover { background: var(--button-bg-hover); }这套分层的好处用数据说话:某设计系统接入分层令牌后,新增一套品牌主题平均改动从约 120 处色值引用降到只改 18 个语义令牌,主题工时下降约 85%。
变量开关技巧:用变量做"CSS 布尔逻辑"
一个进阶技巧(Lea Verou 提出的 space toggle):利用"空值让整段声明作废、非空值让声明生效"的特性,用变量当开关,不写一个媒体查询或 JS 就能切换成组样式。
/* --on 为空(初始)时代表"关",设为 initial 之外的值代表"开" */
.card {
--is-featured: ; /* 空格 = 关闭状态 */
--featured-shadow: var(--is-featured) 0 8px 30px rgba(0,0,0,.15);
box-shadow: var(--featured-shadow, none);
border: var(--is-featured, 1px solid #eee);
}
/* 开启:给变量一个非空值,上面所有引用它的声明同时生效 */
.card.is-featured {
--is-featured: initial;
}同理可实现"多值切换器":
.alert {
/* 默认(info) */
--bg: #e7f3ff;
--fg: #0b5cad;
}
.alert[data-type='success'] { --bg: #e6f7ec; --fg: #1a7f37; }
.alert[data-type='warning'] { --bg: #fff8e6; --fg: #9a6700; }
.alert[data-type='danger'] { --bg: #ffebe9; --fg: #cf222e; }
.alert {
background: var(--bg);
color: var(--fg);
border-left: 4px solid var(--fg);
padding: 12px 16px;
}变量 + calc / min / max / clamp 组合
变量真正的威力在于参与运算。把"基准 + 系数"拆开存,能派生出整套一致的比例系统。
:root {
--step: 0.25rem; /* 间距基准单位 4px */
--ratio: 1.25; /* 模块化字号比例(大三度) */
--base-fs: 1rem;
/* 间距阶梯:全部由 --step 派生 */
--sp-1: calc(var(--step) * 1);
--sp-2: calc(var(--step) * 2);
--sp-3: calc(var(--step) * 3);
--sp-4: calc(var(--step) * 4);
--sp-6: calc(var(--step) * 6);
--sp-8: calc(var(--step) * 8);
/* 字号阶梯:按 ratio 逐级放大 */
--fs-0: var(--base-fs);
--fs-1: calc(var(--fs-0) * var(--ratio));
--fs-2: calc(var(--fs-1) * var(--ratio));
--fs-3: calc(var(--fs-2) * var(--ratio));
/* 流式容器宽度:不小于 320,不大于 1200,中间跟随视口 */
--container: clamp(320px, 90vw, 1200px);
}
.title { font-size: var(--fs-3); margin-bottom: var(--sp-4); }
.subtitle { font-size: var(--fs-1); margin-bottom: var(--sp-2); }
.wrap { width: var(--container); margin-inline: auto; }/* 用变量做"响应式而不写媒体查询"的列数 */
.grid {
--min-col: 240px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(var(--min-col), 100%), 1fr));
gap: var(--sp-4);
}
/* min(--min-col, 100%) 防止单列时超出容器溢出 */变量与动画:错峰动画与交互反馈
把索引、坐标存进变量,能用一套 CSS 实现"错峰入场"等原本要 JS 计算的效果。
/* 列表项依次淡入:每项延迟 = 索引 × 单位延迟 */
.list-item {
opacity: 0;
animation: fade-in 0.4s ease forwards;
animation-delay: calc(var(--i) * 80ms); /* --i 由行内 style 提供 */
}
@keyframes fade-in {
to { opacity: 1; transform: translateY(0); }
}<ul>
<li class="list-item" style="--i: 0">第一项</li>
<li class="list-item" style="--i: 1">第二项</li>
<li class="list-item" style="--i: 2">第三项</li>
<li class="list-item" style="--i: 3">第四项</li>
</ul>/* 跟随鼠标的聚光灯卡片:JS 只更新两个变量,CSS 负责渲染 */
.spotlight {
position: relative;
background:
radial-gradient(
200px circle at var(--mx, 50%) var(--my, 50%),
rgba(59,130,246,.25),
transparent 60%
),
#1e293b;
}const card = document.querySelector('.spotlight');
card.addEventListener('pointermove', (e) => {
const rect = card.getBoundingClientRect();
card.style.setProperty('--mx', (e.clientX - rect.left) + 'px');
card.style.setProperty('--my', (e.clientY - rect.top) + 'px');
});变量与 JavaScript 深度交互
除了 setProperty/getPropertyValue,还有几种进阶用法。
// 1) 批量读取一组令牌,导出给 Canvas/图表库复用同一套配色
function readTokens(names) {
const s = getComputedStyle(document.documentElement);
return Object.fromEntries(
names.map((n) => [n, s.getPropertyValue('--' + n).trim()])
);
}
const theme = readTokens(['color-accent', 'color-bg', 'color-text']);
// 2) 用 rAF 节流高频更新(滚动/鼠标),避免掉帧
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const p = window.scrollY / (document.body.scrollHeight - innerHeight);
document.documentElement.style.setProperty('--scroll', p.toFixed(4));
ticking = false;
});
});
// 3) 从局部元素读取"就近生效"的变量值(体现级联)
const el = document.querySelector('.badge--danger');
const badgeBg = getComputedStyle(el).getPropertyValue('--badge-bg').trim();配合滚动进度变量,可以做无 JS 计算的进度条:
.progress-bar {
transform-origin: left;
transform: scaleX(var(--scroll, 0));
height: 3px;
background: var(--color-accent);
}作用域、继承与 inherits 控制
CSS 变量默认可继承,但注册属性可以关掉继承,这在做"仅本节点生效"的令牌时很有用。
/* 默认继承:父级定义,整棵子树可用 */
.theme-scope { --local-accent: #e91e63; }
.theme-scope .link { color: var(--local-accent); } /* 拿得到 */
/* 关闭继承:只在定义它的元素上生效,子元素取 initial-value */
@property --card-tint {
syntax: '<color>';
inherits: false;
initial-value: transparent;
}利用作用域可以做"局部主题岛",让页面某一块用不同配色而不影响全局:
/* 全站浅色,但"促销专区"内部强制深色,只需在容器上覆盖语义令牌 */
.promo-zone {
--color-bg: #111827;
--color-text: #f9fafb;
--color-accent: #f59e0b;
background: var(--color-bg);
color: var(--color-text);
}多主题与系统主题协同
把"跟随系统"和"用户手动选择"结合,是主题系统的完整形态。
:root {
color-scheme: light;
--bg: #fff;
--fg: #111;
}
/* 用户没手动选时,跟随系统深色 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
color-scheme: dark;
--bg: #111;
--fg: #eee;
}
}
/* 用户手动选择优先级最高 */
[data-theme='light'] { color-scheme: light; --bg: #fff; --fg: #111; }
[data-theme='dark'] { color-scheme: dark; --bg: #111; --fg: #eee; }
body { background: var(--bg); color: var(--fg); }// 三态切换:light → dark → 跟随系统(auto)
function cycleTheme() {
const el = document.documentElement;
const cur = el.getAttribute('data-theme');
const next = cur === 'light' ? 'dark' : cur === 'dark' ? '' : 'light';
if (next) { el.setAttribute('data-theme', next); localStorage.theme = next; }
else { el.removeAttribute('data-theme'); localStorage.removeItem('theme'); }
}
// 首屏尽早应用,避免闪烁(放在 <head> 内联脚本)
const t = localStorage.theme;
if (t) document.documentElement.setAttribute('data-theme', t);性能与工程化考量
更多真实案例
案例 4:图表与页面同源配色。 某数据看板此前 CSS 和 ECharts 各维护一套色板,改主题时两边经常对不上。改为 JS 从 CSS 令牌读取颜色(getComputedStyle 读 --chart-1~--chart-8)传给图表后,主题切换时图表颜色自动同步,配色不一致的 bug 归零。
案例 5:A/B 测试快速换色。 某增长团队要对"主 CTA 按钮"做多组配色 A/B 实验。用变量把按钮色抽成 --cta-bg 后,实验平台只需注入一行 `:root{--cta-bg:#xxx}` 即可切换方案,无需为每个变体重新打包 CSS,实验上线周期从 2 天缩短到分钟级。
案例 6:错峰动画去 JS 化。 某官网首屏 12 个特性卡片原本用 JS 逐个 setTimeout 做入场动画,逻辑散乱且易抖动。改用 `--i` + `animation-delay: calc(var(--i)*60ms)` 后,删掉约 40 行 JS,动画交给合成线程更流畅。
数据与对比:变量方案与替代方案
| 需求 | CSS 变量 | 内联 style | 多份 CSS | class 切换 |
| --- | --- | --- | --- | --- |
| 运行时改主题 | 好(改根变量) | 差(要遍历元素) | 差(要重载) | 一般(预置好类) |
| 网络成本 | 低 | 低 | 高(多份文件) | 低 |
| 首屏闪烁 | 内联脚本可避免 | 无 | 有(切换重载) | 内联可避免 |
| 与 JS 交互 | 强 | 强但笨重 | 弱 | 一般 |
| 维护成本 | 低(单一数据源) | 高 | 高 | 中 |
用变量设计"可配置组件接口"
把组件对外可定制的点抽成局部变量,就像给组件定义了一组"CSS props"。使用者不必了解内部实现,只改这几个变量即可定制。
/* 一个高度可配置的按钮组件 */
.btn {
/* —— 公开接口(有默认值,外部可覆盖) —— */
--btn-bg: var(--color-accent, #3b82f6);
--btn-fg: #fff;
--btn-radius: 8px;
--btn-pad-y: 10px;
--btn-pad-x: 18px;
--btn-font: 14px;
--btn-hover-bg: var(--color-accent-hover, #2563eb);
/* —— 内部实现(用上面的变量拼装) —— */
display: inline-flex;
align-items: center;
gap: 8px;
padding: var(--btn-pad-y) var(--btn-pad-x);
background: var(--btn-bg);
color: var(--btn-fg);
border: none;
border-radius: var(--btn-radius);
font-size: var(--btn-font);
cursor: pointer;
transition: background 0.2s;
}
.btn:hover { background: var(--btn-hover-bg); }
/* 定制变体:只覆盖公开接口,零重复样式 */
.btn--lg { --btn-pad-y: 14px; --btn-pad-x: 26px; --btn-font: 16px; }
.btn--pill { --btn-radius: 999px; }
.btn--danger { --btn-bg: #ef4444; --btn-hover-bg: #dc2626; }
.btn--ghost { --btn-bg: transparent; --btn-fg: var(--color-accent); }同理可做一个卡片组件的可配置接口:
.card {
--card-pad: 20px;
--card-radius: 12px;
--card-bg: var(--color-bg, #fff);
--card-shadow: 0 1px 3px rgba(0,0,0,.1);
--card-accent: var(--color-accent, #3b82f6);
padding: var(--card-pad);
border-radius: var(--card-radius);
background: var(--card-bg);
box-shadow: var(--card-shadow);
border-top: 3px solid var(--card-accent);
}
.card--flat { --card-shadow: none; border: 1px solid #eee; }
.card--compact { --card-pad: 12px; }
.card--warning { --card-accent: #f59e0b; }变量 + 容器查询:组件级自适应令牌
媒体查询看视口,容器查询看父容器。把令牌放进容器查询里改,能让同一组件在不同宽度的容器里自动切换密度。
.card-host {
container-type: inline-size;
container-name: card;
}
.card {
--card-pad: 12px;
--card-cols: 1;
padding: var(--card-pad);
display: grid;
grid-template-columns: repeat(var(--card-cols), 1fr);
gap: var(--card-pad);
}
/* 容器够宽时,同一组件自动变双列、加大内边距 */
@container card (min-width: 480px) {
.card { --card-pad: 20px; --card-cols: 2; }
}不用 Sass:纯 CSS 变量派生工具类系统
结合变量与 calc,可以在纯 CSS 里搭出轻量的间距/字号工具类,无需预处理器循环。
:root {
--space: 4px;
}
/* 间距工具类:值 = 基准 × 档位 */
.p-1 { padding: calc(var(--space) * 1); }
.p-2 { padding: calc(var(--space) * 2); }
.p-3 { padding: calc(var(--space) * 3); }
.p-4 { padding: calc(var(--space) * 4); }
.p-6 { padding: calc(var(--space) * 6); }
.p-8 { padding: calc(var(--space) * 8); }
.gap-2 { gap: calc(var(--space) * 2); }
.gap-4 { gap: calc(var(--space) * 4); }
/* 密度切换:改一个 --space,全套工具类同时缩放 */
.compact { --space: 3px; } /* 紧凑模式 */
.cozy { --space: 6px; } /* 宽松模式 */只要在某个容器上切 `.compact`/`.cozy`,其内部所有工具类的实际间距就跟着变——这是 Sass 编译期变量做不到的运行时能力。
变量在渐变、阴影、SVG 中的应用
变量不只用于单个色值,还能作为渐变、阴影、滤镜、甚至 SVG 属性的组成部分。
:root {
--grad-from: #6366f1;
--grad-to: #ec4899;
--grad-angle: 135deg;
}
.hero {
background: linear-gradient(var(--grad-angle), var(--grad-from), var(--grad-to));
}
/* 多层阴影用变量组合,做"高度系统" */
:root {
--elev-1: 0 1px 2px rgba(0,0,0,.08);
--elev-2: 0 4px 8px rgba(0,0,0,.12);
--elev-3: 0 10px 24px rgba(0,0,0,.16);
}
.dialog { box-shadow: var(--elev-3); }
.dropdown { box-shadow: var(--elev-2); }内联 SVG 也能读取 CSS 变量(需通过 CSS 设置 fill/stroke):
/* 图标颜色跟随主题令牌,一处改色所有图标同步 */
.icon { fill: var(--color-accent, currentColor); }<svg class="icon" viewBox="0 0 24 24" width="24" height="24">
<path d="M12 2 2 22h20L12 2z" />
</svg>变量、unset 与 revert
理解变量与全局关键字的关系有助于排查"变量突然失效"。
.reset-scope {
/* 清掉本作用域的某个令牌,让内部回退到 fallback 或继承 */
--card-accent: initial;
}
.card { border-top: 3px solid var(--card-accent, #ccc); } /* 取到 #ccc */命名规范详解
好的命名能让令牌系统自解释。推荐 `--[类别]-[角色]-[状态/变体]` 的结构。
| 类别前缀 | 示例 | 说明 |
| --- | --- | --- |
| color | --color-text-muted | 颜色语义令牌 |
| space | --space-md | 间距档位 |
| fs / font | --fs-lg | 字号 |
| radius | --radius-pill | 圆角 |
| shadow / elev | --elev-2 | 阴影/层级 |
| z | --z-modal | 层叠顺序 |
| dur / ease | --dur-fast、--ease-out | 动效时长与曲线 |
:root {
--z-dropdown: 1000;
--z-sticky: 1020;
--z-modal: 1050;
--z-toast: 1080;
--dur-fast: 0.15s;
--dur-base: 0.3s;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.modal { z-index: var(--z-modal); transition: opacity var(--dur-base) var(--ease-out); }避免的命名: 表象命名(--blue、--big)会在换主题时变得自相矛盾(深色主题里 --blue 可能其实是浅色);应始终按"角色/语义"命名。
从 Sass 变量迁移到 CSS 变量
老项目常满是 `$` 变量,迁移不必一步到位,可采用"桥接"策略:保留 Sass 变量做编译期逻辑,同时把需要运行时可变的值输出为 CSS 变量。
// 保留 Sass map 做编译期批量生成
$palette: (
'primary': #3b82f6,
'danger': #ef4444,
);
// 桥接:把 Sass 值写进 CSS 变量,运行时就能改
:root {
@each $name, $color in $palette {
--color-#{$name}: #{$color};
}
}
// 组件里用 CSS 变量(运行时可换肤),而不是直接用 $ 变量
.btn { background: var(--color-primary); }迁移判断标准很简单:这个值需要运行时变吗? 需要(主题、用户设置、交互)就迁到 CSS 变量;只在构建期用(循环生成、条件编译、函数计算)就留在 Sass。
常见坑(补充)
变量做状态驱动样式(无需切多个 class)
把状态编码进变量,能用一个变量驱动一组视觉变化,让 JS 逻辑更简洁。
.uploader {
--p: 0; /* 上传进度 0~1,由 JS 更新 */
background: linear-gradient(
to right,
var(--color-accent) calc(var(--p) * 100%),
#e5e7eb 0
);
}
.uploader[data-state='error'] { --color-accent: #ef4444; }
.uploader[data-state='success'] { --color-accent: #22c55e; }// JS 只维护一个数值,视觉全交给 CSS
function setProgress(el, ratio) {
el.style.setProperty('--p', ratio);
el.dataset.state = ratio >= 1 ? 'success' : 'uploading';
}总结
| 你的需求 | 用 CSS 变量怎么做 |
| --- | --- |
| 全站统一主色 | :root 定义 --color-primary,处处 var() |
| 深色/换肤 | [data-theme] 覆盖同名变量 |
| 响应式间距/列数 | 媒体查询里改 :root 变量 |
| 动态/交互样式 | JS setProperty 改变量 |
| 组件可定制接口 | 组件内定义局部变量,外部覆盖 |
| 变量参与动画 | @property 注册类型后再 animate |
| 编译期批量生成 | 交给 Sass,不用 CSS 变量 |
| 组件可配置接口 | 组件内定义局部令牌,变体只覆盖令牌 |
| 变量参与平滑动画 | @property 注册类型后 transition/animation |
| 兜住非法值 | @property 约束类型,而非 var() fallback |
| 局部主题岛 | 容器上覆盖语义令牌,不影响全局 |
| 图表与页面同源配色 | JS getComputedStyle 读令牌传给图表库 |
| 密度/紧凑模式 | 改一个 --space 基准,工具类整体缩放 |
最佳实践清单(补充)
一句话记忆:Sass 变量是"编译期的常量",CSS 变量是"运行时的字段"。凡是需要跟随主题、断点、用户交互动态变化的值,就交给 CSS 变量;需要在构建时做循环、函数、复杂计算的,才交给预处理器变量。二者搭配,才是现代样式工程的完整拼图。