深色模式
CSS Custom Properties(也叫 CSS Variables)已经得到现代浏览器的全面支持。很多人只知道它能定义颜色变量,但它的能力远不止于此。这篇文章介绍 CSS Custom Properties 的高级用法和实战技巧。
基础回顾
css
/* 声明:必须以 -- 开头 */
:root {
--primary-color: #3498db;
--primary-dark: #2980b9;
--font-size-base: 16px;
--spacing-unit: 8px;
--border-radius: 4px;
--transition-speed: 0.3s;
}
/* 使用:通过 var() 函数 */
.button {
background-color: var(--primary-color);
font-size: var(--font-size-base);
padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3);
border-radius: var(--border-radius);
transition: background-color var(--transition-speed) ease;
}
.button:hover {
background-color: var(--primary-dark);
}fallback 值
var() 函数支持第二个参数作为默认值。
css
/* 如果 --primary-color 未定义,使用 #3498db */
.element {
color: var(--primary-color, #3498db);
}
/* fallback 可以嵌套 */
.element {
/* 先尝试 --primary-color,没有就用 --brand-color,再没有就用红色 */
color: var(--primary-color, var(--brand-color, red));
}
/* 实战:组件库的默认值设计 */
.card {
--card-bg: var(--card-background, #ffffff);
--card-shadow: var(--shadow, 0 2px 8px rgba(0, 0, 0, 0.1));
--card-radius: var(--border-radius, 8px);
background: var(--card-bg);
box-shadow: var(--card-shadow);
border-radius: var(--card-radius);
}
/* 使用时可以覆盖,也可以不覆盖用默认值 */
.promo-card {
--card-background: #fffbe6;
--shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}calc() 与变量组合
这是 CSS Custom Properties 最强大的特性之一。
css
:root {
--base-size: 16px;
--scale-ratio: 1.25;
--spacing: 8px;
--column-count: 12;
--container-max: 1200px;
}
/* 排版系统:用一个 base 和 ratio 生成完整的字号阶梯 */
.heading-1 { font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); }
.heading-2 { font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); }
.heading-3 { font-size: calc(var(--base-size) * var(--scale-ratio) * var(--scale-ratio)); }
.body-text { font-size: var(--base-size); }
.small-text { font-size: calc(var(--base-size) / var(--scale-ratio)); }
/* 间距系统 */
.container {
padding: calc(var(--spacing) * 3);
margin-bottom: calc(var(--spacing) * 2);
max-width: var(--container-max);
}
/* 栅格系统 */
.column {
/* 每列宽度 = (容器最大宽度 - (列数+1) * 间距) / 列数 */
width: calc(
(var(--container-max) - (var(--column-count) + 1) * var(--spacing)) /
var(--column-count)
);
margin-right: var(--spacing);
}
.column:last-child {
margin-right: 0;
}
/* 混合单位计算 */
.hero {
/* 100vh 减去 80px 的头部高度 */
min-height: calc(100vh - 80px);
/* 用变量替代固定值 */
--header-height: 80px;
min-height: calc(100vh - var(--header-height));
}
/* 百分比 + 像素混合 */
.sidebar {
--sidebar-width: 250px;
--content-gap: 24px;
width: var(--sidebar-width);
}
.content {
/* 计算剩余宽度 */
width: calc(100% - var(--sidebar-width) - var(--content-gap));
margin-left: var(--content-gap);
}主题切换
最实用的场景:暗色主题。
css
/* 浅色主题(默认) */
:root {
--color-bg: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-text: #333333;
--color-text-secondary: #666666;
--color-border: #e0e0e0;
--color-primary: #3498db;
--color-success: #27ae60;
--color-warning: #f39c12;
--color-danger: #e74c3c;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* 暗色主题 */
[data-theme="dark"] {
--color-bg: #1a1a2e;
--color-bg-secondary: #16213e;
--color-text: #e0e0e0;
--color-text-secondary: #a0a0a0;
--color-border: #333355;
--color-primary: #5dade2;
--color-success: #58d68d;
--color-warning: #f5b041;
--color-danger: #ec7063;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.25);
}
/* 组件只需使用变量,不需要关心当前主题 */
body {
background-color: var(--color-bg);
color: var(--color-text);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background: var(--color-bg);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
padding: 24px;
border-radius: 8px;
}
.card-title {
color: var(--color-text);
}
.card-description {
color: var(--color-text-secondary);
}
.alert-success {
background-color: var(--color-success);
color: #fff;
padding: 12px 16px;
border-radius: 4px;
}
.alert-danger {
background-color: var(--color-danger);
color: #fff;
padding: 12px 16px;
border-radius: 4px;
}配合 JavaScript 切换:
javascript
// 切换主题
function toggleTheme() {
const html = document.documentElement
const current = html.getAttribute('data-theme')
const next = current === 'dark' ? 'light' : 'dark'
html.setAttribute('data-theme', next)
localStorage.setItem('theme', next)
}
// 初始化:读取本地存储或系统偏好
function initTheme() {
const saved = localStorage.getItem('theme')
if (saved) {
document.documentElement.setAttribute('data-theme', saved)
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark')
}
}
initTheme()通过 JavaScript 读写变量
CSS Custom Properties 可以在 JavaScript 中读写,这打通了 CSS 和 JS 的桥梁。
javascript
// 读取变量值
const root = getComputedStyle(document.documentElement)
const primaryColor = root.getPropertyValue('--primary-color').trim()
console.log(primaryColor) // '#3498db'
// 设置变量值
document.documentElement.style.setProperty('--primary-color', '#e74c3c')
// 删除变量
document.documentElement.style.removeProperty('--primary-color')
// 作用于特定元素
const card = document.querySelector('.card')
card.style.setProperty('--card-bg', '#fffbe6')
// 实战:动态修改间距系统
function updateSpacing(baseSpacing) {
document.documentElement.style.setProperty('--spacing-unit', `${baseSpacing}px`)
}
// 实战:运行时调整字号(无障碍功能)
function increaseFontSize() {
const root = getComputedStyle(document.documentElement)
const current = parseFloat(root.getPropertyValue('--base-font-size'))
document.documentElement.style.setProperty('--base-font-size', `${current + 2}px`)
}
// 实战:从 CSS 读值到 JS 使用
function getThemeColors() {
const styles = getComputedStyle(document.documentElement)
return {
bg: styles.getPropertyValue('--color-bg').trim(),
text: styles.getPropertyValue('--color-text').trim(),
primary: styles.getPropertyValue('--color-primary').trim()
}
}作用域与继承
CSS Custom Properties 遵循 CSS 的级联和继承规则,这是它的独特优势。
css
/* 全局变量 */
:root {
--color: blue;
--font-size: 16px;
}
/* 局部覆盖 */
.sidebar {
--color: green;
--font-size: 14px;
}
/* .sidebar 内部的所有元素继承绿色 */
.sidebar p {
color: var(--color); /* green */
font-size: var(--font-size); /* 14px */
}
/* 更深层的覆盖 */
.sidebar .special {
--color: red;
color: var(--color); /* red */
}
/* 组件级别的变量:BEM + Custom Properties */
/* BEM 命名 + Custom Properties 是很好的组合 */
.button {
--btn-padding: 10px 20px;
--btn-bg: var(--primary-color);
--btn-color: #ffffff;
--btn-radius: 4px;
padding: var(--btn-padding);
background: var(--btn-bg);
color: var(--btn-color);
border-radius: var(--btn-radius);
}
.button--large {
--btn-padding: 14px 28px;
--btn-radius: 6px;
}
.button--danger {
--btn-bg: #e74c3c;
}
.button--outline {
--btn-bg: transparent;
--btn-color: var(--primary-color);
border: 2px solid var(--btn-bg);
}响应式设计
结合媒体查询使用变量。
css
:root {
--container-padding: 16px;
--grid-columns: 4;
--font-size-base: 14px;
--sidebar-width: 240px;
}
/* 平板 */
@media (min-width: 768px) {
:root {
--container-padding: 24px;
--grid-columns: 8;
--font-size-base: 15px;
}
}
/* 桌面 */
@media (min-width: 1024px) {
:root {
--container-padding: 32px;
--grid-columns: 12;
--font-size-base: 16px;
--sidebar-width: 280px;
}
}
/* 大屏 */
@media (min-width: 1440px) {
:root {
--container-padding: 48px;
--sidebar-width: 320px;
}
}
/* 使用变量的组件会自动适应断点 */
.page {
padding: var(--container-padding);
font-size: var(--font-size-base);
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: calc(var(--container-padding) / 2);
}
.sidebar {
width: var(--sidebar-width);
}实用技巧
透明度变体
css
:root {
--primary-color: 52, 152, 219; /* RGB 值,不带 rgb() */
}
.element {
background-color: rgb(var(--primary-color));
/* 通过 rgba 实现透明度变体,不需要额外定义变量 */
border-color: rgba(var(--primary-color), 0.3);
box-shadow: 0 2px 8px rgba(var(--primary-color), 0.2);
}
.hover-state {
background-color: rgba(var(--primary-color), 0.1);
}动画控制
css
:root {
/* 用变量控制动画 */
--animation-duration: 0.3s;
--animation-easing: cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-in {
animation: fadeIn var(--animation-duration) var(--animation-easing);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 全局调整动画速度(比如用户偏好减少动画) */
@media (prefers-reduced-motion: reduce) {
:root {
--animation-duration: 0.01s;
}
}CSS 自定义属性与 Grid
css
:root {
--grid-gap: 16px;
--card-min-width: 280px;
}
.auto-grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--card-min-width), 1fr)
);
gap: var(--grid-gap);
}
/* 不同区域覆盖最小宽度 */
.featured-grid {
--card-min-width: 360px;
}条件样式(利用无效值)
css
/* 当 --icon-url 有值时显示图标,没有时隐藏 */
.icon {
background-image: var(--icon-url, none);
background-size: contain;
width: var(--icon-size, 24px);
height: var(--icon-size, 24px);
}
/* 使用时 */
.search-input {
--icon-url: url('search.svg');
--icon-size: 20px;
}小结
- CSS Custom Properties 遵循级联和继承规则,天然适合主题系统和组件化开发
calc()与变量组合能实现响应式的排版系统和栅格布局- 通过 JavaScript 读写变量,打通了 CSS 和 JS 的桥梁
- 用 fallback 值和作用域特性实现组件库的默认值设计
- 拆分 RGB 值到变量中,用
rgba()实现透明度变体,减少变量数量 - 在
:root上结合媒体查询改变变量值,让所有引用该变量的样式自动适应断点