CSS 变量
大约 11 分钟约 3214 字
CSS 变量
简介
CSS 自定义属性(CSS Variables,也叫 CSS Custom Properties)允许在样式表中定义可复用的值,支持运行时动态修改和 JavaScript 操作。与 Sass/Less 变量(编译时替换)不同,CSS 变量是运行时特性,可以在浏览器中实时改变。它是实现主题切换、设计令牌(Design Tokens)和组件样式定制的核心机制。CSS 变量遵循 CSS 层叠规则,子元素可以覆盖父元素的变量值,实现细粒度的样式控制。
特点
实现
设计令牌系统
/* ========== :root 全局变量 ========== */
:root {
/* 颜色 — 主色 */
--color-primary: #1890ff;
--color-primary-hover: #40a9ff;
--color-primary-active: #096dd9;
--color-primary-light: #e6f7ff;
--color-primary-bg: #f0f5ff;
/* 颜色 — 功能色 */
--color-success: #52c41a;
--color-success-hover: #73d13d;
--color-success-light: #f6ffed;
--color-warning: #faad14;
--color-warning-hover: #ffc53d;
--color-warning-light: #fffbe6;
--color-error: #ff4d4f;
--color-error-hover: #ff7875;
--color-error-light: #fff2f0;
--color-info: #1890ff;
--color-info-light: #e6f7ff;
/* 颜色 — 中性色 */
--color-text-primary: #333;
--color-text-secondary: #666;
--color-text-tertiary: #999;
--color-text-disabled: #ccc;
--color-text-inverse: #fff;
--color-border: #d9d9d9;
--color-border-light: #e8e8e8;
--color-divider: #f0f0f0;
--color-bg-primary: #fff;
--color-bg-secondary: #fafafa;
--color-bg-tertiary: #f5f5f5;
--color-bg-hover: #e8e8e8;
/* 间距 */
--space-xxs: 2px;
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-xxl: 48px;
/* 字体 */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
--font-family-mono: 'Fira Code', 'Cascadia Code', Consolas, monospace;
--font-size-xs: 12px;
--font-size-sm: 13px;
--font-size-md: 14px;
--font-size-lg: 16px;
--font-size-xl: 20px;
--font-size-xxl: 24px;
--font-size-display: 32px;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-base: 1.5715;
--line-height-tight: 1.25;
/* 圆角 */
--radius-none: 0;
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-round: 50%;
--radius-pill: 999px;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
--shadow-xl: 0 12px 48px rgba(0, 0, 0, 0.15);
/* 过渡 */
--transition-fast: 0.15s ease;
--transition-base: 0.3s ease;
--transition-slow: 0.5s ease;
/* z-index */
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-popover: 600;
--z-tooltip: 700;
--z-toast: 800;
}
/* ========== 暗色主题 ========== */
[data-theme="dark"] {
--color-primary: #177ddc;
--color-primary-hover: #3c9ae8;
--color-primary-active: #0958ae;
--color-primary-light: #111d2c;
--color-primary-bg: #111b26;
--color-text-primary: #e8e8e8;
--color-text-secondary: #a6a6a6;
--color-text-tertiary: #737373;
--color-text-disabled: #595959;
--color-border: #434343;
--color-border-light: #303030;
--color-divider: #2a2a2a;
--color-bg-primary: #141414;
--color-bg-secondary: #1f1f1f;
--color-bg-tertiary: #262626;
--color-bg-hover: #333;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
}
/* ========== 紧凑主题 ========== */
[data-density="compact"] {
--space-sm: 4px;
--space-md: 8px;
--space-lg: 16px;
--space-xl: 24px;
--font-size-sm: 12px;
--font-size-md: 13px;
--font-size-lg: 14px;
--radius-md: 2px;
--radius-lg: 4px;
}组件级变量与回退
/* ========== 组件级变量 — 允许外部定制 ========== */
.card {
/* 内部变量(可被外部覆盖) */
--card-padding: var(--space-md);
--card-radius: var(--radius-lg);
--card-bg: var(--color-bg-primary);
--card-border: var(--color-border-light);
--card-shadow: var(--shadow-sm);
padding: var(--card-padding);
border-radius: var(--card-radius);
background: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow: var(--card-shadow);
transition: box-shadow var(--transition-base);
}
.card:hover {
box-shadow: var(--shadow-md);
}
/* 紧凑模式 — 覆盖组件级变量 */
.card.compact {
--card-padding: var(--space-sm);
--card-radius: var(--radius-md);
}
/* 外部定制 — 通过 CSS 变量覆盖 */
.custom-card {
--card-padding: 24px;
--card-radius: 16px;
--card-bg: #f0f5ff;
}
/* ========== 回退值 — var() 的第二个参数 ========== */
.btn {
/* 如果 --btn-color 未定义,使用 --color-primary;如果 --color-primary 也未定义,使用 #1890ff */
background: var(--btn-color, var(--color-primary, #1890ff));
color: var(--btn-text, #fff);
padding: var(--btn-padding, 8px 16px);
border-radius: var(--btn-radius, var(--radius-md));
font-size: var(--btn-font-size, var(--font-size-md));
}
/* 嵌套回退 */
.some-element {
/* 多层回退链 */
color: var(--custom-color, var(--text-color, var(--color-text-primary, #333)));
}
/* ========== 表单组件变量 ========== */
.input {
--input-height: 36px;
--input-padding: 0 var(--space-md);
--input-font-size: var(--font-size-md);
--input-border: var(--color-border);
--input-focus-border: var(--color-primary);
--input-focus-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
height: var(--input-height);
padding: var(--input-padding);
font-size: var(--input-font-size);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
outline: none;
transition: all var(--transition-fast);
}
.input:focus {
border-color: var(--input-focus-border);
box-shadow: var(--input-focus-shadow);
}
.input.error {
--input-border: var(--color-error);
--input-focus-border: var(--color-error);
--input-focus-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}JavaScript 动态操作
// ========== 获取变量值 ==========
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles.getPropertyValue('--color-primary').trim();
const spaceMd = styles.getPropertyValue('--space-md').trim();
// ========== 设置变量值 ==========
document.documentElement.style.setProperty('--color-primary', '#e74c3c');
document.documentElement.style.setProperty('--space-md', '20px');
// ========== 主题切换 ==========
function setTheme(theme: 'light' | 'dark' | 'auto') {
if (theme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme = prefersDark ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
// 初始化主题
function initTheme() {
const saved = localStorage.getItem('theme');
if (saved && saved !== 'auto') {
setTheme(saved as 'light' | 'dark');
} else {
// 跟随系统
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem('theme') === 'auto') {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
// ========== 动态生成颜色 ==========
// 基于主色生成完整色阶
function generateColorScale(baseColor: string) {
const root = document.documentElement;
root.style.setProperty('--color-primary', baseColor);
// 可以使用 color-mix 或 JavaScript 颜色库生成浅色/深色变体
root.style.setProperty('--color-primary-light', `${baseColor}15`);
root.style.setProperty('--color-primary-hover', lighten(baseColor, 10));
root.style.setProperty('--color-primary-active', darken(baseColor, 10));
}
// ========== 读取组件级变量 ==========
function getCardPadding(element: HTMLElement): string {
const styles = getComputedStyle(element);
return styles.getPropertyValue('--card-padding').trim();
}
// ========== 动态切换密度 ==========
function setDensity(density: 'default' | 'compact' | 'comfortable') {
document.documentElement.setAttribute('data-density', density);
localStorage.setItem('density', density);
}
// ========== React Hook ==========
function useCssVariable(variableName: string) {
const [value, setValue] = useState(() =>
getComputedStyle(document.documentElement).getPropertyValue(variableName).trim()
);
useEffect(() => {
const observer = new MutationObserver(() => {
const newValue = getComputedStyle(document.documentElement)
.getPropertyValue(variableName).trim();
if (newValue !== value) setValue(newValue);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class', 'data-theme'],
});
return () => observer.disconnect();
}, [variableName, value]);
return value;
}@property 注册自定义属性
/* @property — 注册自定义属性的类型和初始值 */
@property --color-primary {
syntax: '<color>';
inherits: true;
initial-value: #1890ff;
}
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
/* @property 允许动画过渡 */
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.animated-gradient {
--gradient-angle: 0deg;
background: linear-gradient(var(--gradient-angle), #1890ff, #52c41a, #faad14);
animation: rotate-gradient 3s linear infinite;
}
@keyframes rotate-gradient {
to { --gradient-angle: 360deg; }
}
/* 没有注册 @property 时,CSS 变量的颜色变化无法过渡 */
.registered-transition {
--hue: 0;
color: hsl(var(--hue), 70%, 50%);
transition: --hue 0.5s; /* 需要 @property 注册才能过渡 */
}响应式 CSS 变量
/* 响应式间距 */
:root {
--container-padding: var(--space-lg);
}
@media (max-width: 768px) {
:root {
--container-padding: var(--space-md);
}
}
@media (max-width: 480px) {
:root {
--container-padding: var(--space-sm);
}
}
/* 响应式字体 */
:root {
--font-size-body: var(--font-size-md);
}
@media (min-width: 1600px) {
:root {
--font-size-body: var(--font-size-lg);
}
}
/* 无障碍 — 减少动画 */
@media (prefers-reduced-motion: reduce) {
:root {
--transition-fast: 0.01ms;
--transition-base: 0.01ms;
--transition-slow: 0.01ms;
}
}优点
缺点
总结
CSS 变量是实现主题系统和设计令牌的核心机制。通过 :root 定义全局变量,组件级变量支持细粒度定制。JavaScript 可通过 setProperty 动态修改变量值实现运行时主题切换。@property 注册自定义属性类型后支持过渡动画。建议统一变量命名规范,建立完整的设计令牌系统。
关键知识点
- CSS 变量遵循层叠规则,子元素可以覆盖父元素的变量。
- var() 的第二个参数是回退值,当变量未定义时使用。
- :root 上的变量等同于全局变量,任何选择器都可以访问。
- CSS 变量是运行时特性,与 Sass/Less 变量(编译时)不同。
- @property 注册后,CSS 变量值可以参与过渡动画。
项目落地视角
- 建立设计令牌系统,统一颜色、间距、字体等变量。
- 主题切换通过修改 :root 变量实现,不需要额外 CSS 文件。
- 使用 data-theme 属性区分主题。
常见误区
- 混淆 CSS 变量和 Sass 变量的作用时机。
- 在选择器中定义过多变量导致调试困难。
- 忘记为暗色主题覆盖所有颜色变量。
进阶路线
- 学习 Design Tokens 和 Style Dictionary 标准化设计系统。
- 研究容器查询(@container)与 CSS 变量的配合。
- 了解 @property 规则注册自定义属性类型。
适用场景
- 亮色/暗色主题切换。
- 设计系统中的 Design Tokens 管理。
- 组件库的可定制主题。
- 响应式设计中的断点变量。
落地建议
- 统一变量命名规范(--color-, --space-, --font-*)。
- 为所有主题相关的颜色使用 CSS 变量。
- 主题切换后持久化到 localStorage。
排错清单
- 检查变量名是否拼写正确(区分大小写)。
- 确认变量定义的作用域是否正确。
- 检查 JavaScript setProperty 的变量名是否包含 --。
复盘问题
- CSS 变量和 Tailwind 自定义主题哪种方式更适合你的项目?
- 主题切换是否需要过渡动画?如何实现?
- 如何确保新增颜色都使用 CSS 变量而不是硬编码值?
CSS 变量性能优化
减少重绘与回流
/* ========== 性能友好的 CSS 变量使用 ========== */
/* 好的做法 — 变量影响合成层属性(transform/opacity) */
.card {
--card-scale: 1;
--card-opacity: 1;
transform: scale(var(--card-scale));
opacity: var(--card-opacity);
transition: transform 0.3s ease, opacity 0.3s ease;
will-change: transform, opacity;
}
.card:hover {
--card-scale: 1.05;
}
.card.hidden {
--card-opacity: 0;
pointer-events: none;
}
/* 避免 — 变量频繁触发布局(width/height/margin) */
/* .layout { --sidebar-width: 250px; width: var(--sidebar-width); } */
/* JavaScript 频繁修改 --sidebar-width 会触发 layout thrashing */CSS 变量与 calc() 结合
/* ========== calc() 与 CSS 变量 ========== */
.grid {
--columns: 3;
--gap: var(--space-md);
--sidebar-width: 260px;
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--gap);
}
/* 侧边栏布局 */
.layout-with-sidebar {
--sidebar-width: 280px;
--content-padding: var(--space-lg);
display: grid;
grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width));
min-height: 100vh;
}
.content {
padding: var(--content-padding);
/* 减去 header 高度 */
height: calc(100vh - var(--header-height, 60px));
}
/* 响应式字体 — clamp() 与变量结合 */
:root {
--font-min: 14px;
--font-max: 18px;
--font-preferred: clamp(var(--font-min), 0.5vw + 12px, var(--font-max));
}
body {
font-size: var(--font-preferred);
}
/* 动态计算容器宽度 */
.container {
--container-max: 1200px;
--container-padding: var(--space-md);
width: min(var(--container-max), 100% - var(--container-padding) * 2);
margin-inline: auto;
}CSS 变量与容器查询
/* ========== 容器查询 + CSS 变量 ========== */
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
--card-direction: row;
--card-image-width: 200px;
}
}
@container card (max-width: 399px) {
.card {
--card-direction: column;
--card-image-width: 100%;
}
}
.card {
display: flex;
flex-direction: var(--card-direction, column);
}
.card-image {
width: var(--card-image-width, 100%);
aspect-ratio: 16 / 9;
object-fit: cover;
}CSS 变量与 Web Components
Shadow DOM 中的变量穿透
/* ========== Web Components 中的 CSS 变量 ========== */
/* 外部定义变量(可以穿透 Shadow DOM 边界) */
:host {
--button-bg: var(--color-primary, #1890ff);
--button-text: #fff;
--button-radius: var(--radius-md, 4px);
--button-padding: 8px 16px;
--button-font-size: var(--font-size-md, 14px);
}
/* 使用 CSS 自定义属性实现 Web Component 主题定制 */
custom-button {
--button-bg: #52c41a;
--button-radius: var(--radius-pill, 999px);
}CSS 变量的继承与覆盖策略
/* ========== 多层级变量覆盖 ========== */
/* 全局默认值 */
:root {
--card-bg: #fff;
--card-padding: var(--space-md);
--card-radius: var(--radius-lg);
}
/* 暗色主题覆盖 */
[data-theme="dark"] {
--card-bg: #1f1f1f;
}
/* 页面级覆盖 */
.page-dashboard {
--card-padding: var(--space-lg);
}
/* 组件级覆盖 */
.card.compact {
--card-padding: var(--space-sm);
}
/* 单个实例覆盖 */
.card.featured {
--card-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-padding: var(--space-xl);
}
/* 变量继承链::root -> [data-theme] -> .page -> .card -> .card.compact */
/* 子元素的变量值 = 最近的祖先元素中定义的值 */CSS 变量在大型项目中的管理
变量命名规范
/* ========== 推荐的命名规范 ========== */
/* 格式:--{namespace}-{category}-{property}-{variant} */
/* 命名空间前缀(可选,适合组件库) */
/* --myui-color-primary */
/* 颜色:--color-{semantic}-{variant} */
--color-primary
--color-primary-hover
--color-primary-active
--color-primary-light
--color-primary-bg
/* 间距:--space-{size} */
--space-xs
--space-sm
--space-md
--space-lg
--space-xl
/* 字体:--font-{property}-{variant} */
--font-family
--font-family-mono
--font-size-sm
--font-weight-medium
/* 组件变量:--{component}-{property} */
--card-padding
--card-radius
--card-bg
--card-shadow
--button-height
--button-padding
--input-border
--input-focus-shadow
/* z-index:--z-{layer} */
--z-dropdown
--z-modal
--z-tooltipSCSS 自动生成 CSS 变量
// SCSS mixin 自动生成 CSS 变量
@mixin define-color-tokens($colors) {
:root {
@each $name, $value in $colors {
--color-#{$name}: #{$value};
}
}
}
// 定义色板
$color-palette: (
'primary': #1890ff,
'success': #52c41a,
'warning': #faad14,
'error': #ff4d4f,
);
@include define-color-tokens($color-palette);
// 自动生成 hover/active/light 变体
@mixin color-variants($name, $base) {
--color-#{$name}: #{$base};
--color-#{$name}-hover: lighten($base, 8%);
--color-#{$name}-active: darken($base, 8%);
--color-#{$name}-light: rgba($base, 0.08);
--color-#{$name}-bg: rgba($base, 0.04);
}