从预处理器变量到 CSS 自定义属性,再到二者的协同作战——这是一份全面的前端样式工程化指南。
目录
1. CSS 自定义属性(CSS Variables)
1.1 声明与使用
CSS 自定义属性(Custom Properties)由 CSS 规范原生定义,以 -- 作为前缀声明,通过 var() 函数读取。
基本语法:
/* 声明 */
:root {
--primary-color: #3498db;
--spacing-base: 8px;
--font-size-large: 1.5rem;
}
/* 使用 */
.button {
background-color: var(--primary-color);
margin: var(--spacing-base);
font-size: var(--font-size-large);
}var() 函数接受两个参数:
.element {
/* var(<自定义属性名>, <回退值>) */
color: var(--undefined-var, #333); /* fallback 生效:#333 */
background: var(--primary-color, blue);
}回退值可以嵌套使用:
.element {
font-family: var(--font-primary, var(--font-fallback, sans-serif));
}⚠️ 注意: var() 的 fallback 不支持多个值的简写属性分段回退。例如以下写法是错误的:
/* ❌ 错误:var() 内不能包含多值回退中的部分值 */
.border {
border: var(--border-width, 1px) solid var(--border-color, #000);
/* 如果 --border-width 未定义,只会得到 "1px solid",不会自动补 solid */
}1.2 作用域与继承
CSS 变量的作用域遵循 CSS 级联规则——变量在离元素最近的作用域中生效。
:root {
--accent: #e74c3c; /* 全局变量 */
}
.card {
--accent: #2ecc71; /* 局部变量,覆盖 .card 及其子元素 */
--radius: 8px; /* 仅 .card 作用域内可见 */
border-radius: var(--radius);
}
.card-title {
color: var(--accent); /* 继承 .card 的值,即 #2ecc71 */
}
.card-footer {
--accent: #f39c12; /* 更具体的局部覆盖 */
--radius: 4px; /* 仅在 .card-footer 内 */
color: var(--accent); /* #f39c12 */
}继承链示例:
<div class="parent" style="--level: 1;">
<div class="child" style="--level: 2;">
<div class="grandchild">
<p>Level: var(--level)</p> <!-- 继承 .child 的值:2 -->
</div>
</div>
</div>变量的继承特性使其非常适合设计系统中的主题令牌管理:
:root {
/* 基础调色板 */
--color-primary-100: #d6eaf8;
--color-primary-500: #3498db;
--color-primary-900: #1a5276;
/* 语义化令牌,引用基础调色板 */
--color-bg-primary: var(--color-primary-100);
--color-text-link: var(--color-primary-500);
--color-border-focus: var(--color-primary-900);
}1.3 与预处理器变量的区别
这是整篇文章的核心比较,理解二者的本质区别至关重要。
| 特性 | CSS 自定义属性 | Sass/Less 变量 |
|---|---|---|
| 解析时机 | 运行时(浏览器) | 编译时 |
| 动态性 | ✅ 可在运行时修改 | ❌ 编译后固定 |
| 作用域 | DOM 继承(级联) | 词法作用域(大括号) |
| JavaScript 操作 | ✅ 原生支持 | ❌ 需额外工具 |
| 媒体查询 | ✅ 可在查询内改变 | ❌ 不能直接使用 |
| 动画过渡 | ✅ 可与 transition/animation 配合 | ❌ 编译后为固定值 |
| 类型 | 字符串(浏览器不校验类型) | 有类型概念(数字、颜色、列表) |
| 计算能力 | 需配合 calc() |
任意表达式直接运算 |
| 回退机制 | 内置 var(, fallback) | 无原生回退 |
代码对比:
// Sass/SCSS — 编译时确定
$primary: #3498db;
$padding: 16px;
.button {
background: $primary;
padding: $padding;
}
// 编译后:
// .button { background: #3498db; padding: 16px; }/* CSS 自定义属性 — 运行时动态 */
:root {
--primary: #3498db;
--padding: 16px;
}
.button {
background: var(--primary);
padding: var(--padding);
}
/* 浏览器解析,--primary 可在任何时刻被修改 */本质差异一句话: 预处理器变量是编译期的宏替换,CSS 自定义属性是运行时的动态值。
1.4 JavaScript 操作
CSS 变量最大的优势之一——可以通过 JavaScript 在运行时动态读写。
读取变量:
const root = document.documentElement; // :root
const styles = getComputedStyle(root);
const primary = styles.getPropertyValue('--primary-color').trim();
console.log(primary); // "#3498db"
// 读取任意元素的变量
const card = document.querySelector('.card');
const cardRadius = getComputedStyle(card).getPropertyValue('--radius').trim();设置变量:
const root = document.documentElement;
root.style.setProperty('--primary-color', '#e74c3c');
// 批量更新主题
function applyTheme(theme) {
const themeMap = {
light: {
'--bg': '#ffffff',
'--text': '#333333',
'--border': '#dddddd',
},
dark: {
'--bg': '#1a1a2e',
'--text': '#e0e0e0',
'--border': '#333333',
},
};
const vars = themeMap[theme];
const root = document.documentElement;
Object.entries(vars).forEach(([name, value]) => {
root.style.setProperty(name, value);
});
}
applyTheme('dark');移除变量:
root.style.removeProperty('--primary-color');1.5 动态主题切换实战(暗色模式)
完整的暗色模式实现是 CSS 变量实战的最佳场景。
方案一:通过 class 切换
/* 基础变量 */
:root,
[data-theme='light'] {
--bg-primary: #ffffff;
--bg-secondary: #f5f6fa;
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
--accent: #3498db;
--accent-hover: #2980b9;
--border: #e1e8ed;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
[data-theme='dark'] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #e8e8e8;
--text-secondary: #a0a0a0;
--accent: #64b5f6;
--accent-hover: #42a5f5;
--border: #2a2a4a;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
/* 使用变量 */
body {
background: var(--bg-primary);
color: var(--text-primary);
transition: background 0.3s ease, color 0.3s ease;
}
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.button {
background: var(--accent);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.button:hover {
background: var(--accent-hover);
}方案二:配合 prefers-color-scheme 媒体查询(自动跟随系统)
:root {
--bg: #ffffff;
--text: #222222;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a2e;
--text: #e0e0e0;
}
}JavaScript 切换逻辑:
const themeToggle = document.getElementById('theme-toggle');
// 检测系统偏好
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
// 应用主题
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
// 切换主题
themeToggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
setTheme(current === 'dark' ? 'light' : 'dark');
});
// 初始化:优先使用本地存储,其次系统偏好
const saved = localStorage.getItem('theme');
if (saved) {
setTheme(saved);
} else {
setTheme(getSystemTheme());
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
// 只有用户没有手动设置时跟随系统
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});1.6 响应式变量(在媒体查询中改变变量值)
CSS 变量可以在媒体查询中重新赋值,从而实现基于视口尺寸的样式变化——这是预处理器变量无法做到的。
:root {
--grid-gap: 24px;
--container-padding: 40px;
--font-size-base: 18px;
--card-min-width: 320px;
}
/* 平板 */
@media (max-width: 1024px) {
:root {
--grid-gap: 16px;
--container-padding: 24px;
--font-size-base: 16px;
--card-min-width: 280px;
}
}
/* 手机 */
@media (max-width: 640px) {
:root {
--grid-gap: 12px;
--container-padding: 16px;
--font-size-base: 14px;
--card-min-width: 100%;
}
}
/* 使用变量构建响应式网格 */
.grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--card-min-width), 1fr)
);
gap: var(--grid-gap);
padding: var(--container-padding);
font-size: var(--font-size-base);
}更复杂的场景:基于容器查询(Container Queries)
.card-container {
container-type: inline-size;
}
@container (max-width: 400px) {
.card {
--card-layout: vertical;
--card-image-height: 200px;
--card-padding: 12px;
}
}
@container (min-width: 401px) {
.card {
--card-layout: horizontal;
--card-image-height: 100%;
--card-padding: 24px;
}
}
.card {
display: flex;
flex-direction: var(--card-layout, horizontal);
padding: var(--card-padding);
}1.7 计算:calc() + var() 结合
CSS 变量是纯文本值,计算必须通过 calc() 函数完成。
:root {
--spacing-unit: 8px;
--scale: 1.2;
--base-size: 1rem;
}
.card {
/* 四则运算 */
padding: calc(var(--spacing-unit) * 3); /* 24px */
margin: calc(var(--spacing-unit) + 4px); /* 12px */
width: calc(100% - var(--spacing-unit) * 4); /* 100% - 32px */
}
/* 构建模块化间距系统 */
.space-xs { margin: calc(var(--spacing-unit) * 0.5); }
.space-sm { margin: calc(var(--spacing-unit) * 1); }
.space-md { margin: calc(var(--spacing-unit) * 2); }
.space-lg { margin: calc(var(--spacing-unit) * 4); }
.space-xl { margin: calc(var(--spacing-unit) * 8); }
/* 字体缩放 */
h1 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale) * var(--scale) * var(--scale)); }
h2 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale) * var(--scale)); }
h3 { font-size: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale)); }
h4 { font-size: calc(var(--base-size) * var(--scale) * var(--scale)); }
/* 常见陷阱:单位不一致 */
:root {
--gap: 10; /* 纯数字,不带单位 */
}
/* ❌ 错误:calc() 中两个操作数单位不一致 */
.box { gap: calc(var(--gap) + 10px); } /* 10 (无单位) + 10px → 无效 */
/* ✅ 正确:确保单位一致 */
:root {
--gap: 10px;
}
.box { gap: calc(var(--gap) + 10px); } /* 20px */
/* 或者乘以 1px */
:root {
--gap: 10;
}
.box { gap: calc(var(--gap) * 1px + 10px); } /* 20px */1.8 @property —— 注册类型化自定义属性
@property 是 CSS Houdini 的一部分,允许开发者注册具有类型的 CSS 自定义属性,支持初始值、继承行为,并且让变量可以被 CSS 动画和过渡作用。
基本语法:
@property --rotation-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --color-start {
syntax: '<color>';
inherits: true;
initial-value: #ff6b6b;
}
@property --color-end {
syntax: '<color>';
inherits: true;
initial-value: #4ecdc4;
}
@property --border-radius {
syntax: '<length-percentage>';
inherits: false;
initial-value: 0px;
}支持的类型(syntax):
| 类型 | 描述 | 示例值 |
|---|---|---|
<length> |
长度 | 10px, 2em |
<number> |
数字 | 42, 3.14 |
<percentage> |
百分比 | 50% |
<length-percentage> |
长度或百分比 | 10px, 50% |
<color> |
颜色 | #ff0, rgb(255,0,0) |
<angle> |
角度 | 45deg, 0.5turn |
<time> |
时间 | 2s, 500ms |
<string> |
字符串 | "hello" |
<custom-ident> |
自定义标识符 | ease-in-out |
+ |
空格分隔列表 | <color>+ |
# |
逗号分隔列表 | <color># |
实战:让变量可动画
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --scale-factor {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
.box {
--gradient-angle: 0deg;
--scale-factor: 1;
width: 200px;
height: 200px;
background: linear-gradient(
var(--gradient-angle),
#ff6b6b,
#4ecdc4
);
transform: scale(var(--scale-factor));
border-radius: 16px;
/* 现在这些属性可以过渡和动画! */
transition: --gradient-angle 0.5s ease,
--scale-factor 0.3s ease;
}
.box:hover {
--gradient-angle: 180deg;
--scale-factor: 1.1;
}
/* 关键帧动画 */
@keyframes rotate-gradient {
from {
--gradient-angle: 0deg;
}
to {
--gradient-angle: 360deg;
}
}
.box-animated {
animation: rotate-gradient 3s linear infinite;
}浏览器兼容性处理:
/* 回退方案:不使用 @property */
.box-fallback {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
transition: background 0.5s ease;
}
.box-fallback:hover {
background: linear-gradient(180deg, #ff6b6b, #4ecdc4);
}
/* 渐进增强:使用 @property(浏览器支持时生效) */
@supports (--custom: property) {
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 45deg;
}
.box-enhanced {
--gradient-angle: 45deg;
background: linear-gradient(
var(--gradient-angle),
#ff6b6b,
#4ecdc4
);
transition: --gradient-angle 0.5s ease;
}
.box-enhanced:hover {
--gradient-angle: 180deg;
}
}2. Sass/SCSS 深度指南
Sass(Syntactically Awesome Style Sheets)是目前最成熟、生态最完善的 CSS 预处理器,有 .sass(缩进语法)和 .scss(类 CSS 语法)两种格式。推荐使用 SCSS,它与 CSS 完全兼容,学习曲线更平缓。
2.1 变量
Sass 变量以 $ 开头,编译时替换为最终值。
// 基本变量
$primary-color: #3498db;
$font-stack: 'Inter', -apple-system, sans-serif;
$base-padding: 16px;
$enable-rounded: true;
// 变量引用变量
$link-color: $primary-color;
$link-hover-color: darken($primary-color, 10%);
.button {
color: $primary-color;
font-family: $font-stack;
padding: $base-padding;
@if $enable-rounded {
border-radius: 8px;
}
}作用域规则:
// 全局变量
$global-color: #333;
.container {
// 局部变量
$local-color: #666;
// 在嵌套作用域内重新声明全局变量(谨慎使用)
$global-color: #111 !global;
color: $local-color; // #666
border-color: $global-color; // #111
}
// 编译后:
// .container { color: #666; border-color: #111; }!default 标记:
!default 允许在变量已定义时保留原值,未定义时才赋值。这是设计系统和组件库的核心机制。
// _variables.scss — 默认配置
$primary: #3498db !default;
$danger: #e74c3c !default;
$font-size-base: 16px !default;
$border-radius: 4px !default;
$enable-shadows: false !default;
// _buttons.scss — 使用默认值
@import 'variables';
.button {
background: $primary;
font-size: $font-size-base;
border-radius: $border-radius;
@if $enable-shadows {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
// app.scss — 覆盖默认值(必须在 import 前定义)
$primary: #e67e22;
$enable-shadows: true;
@import 'buttons';
// 最终 $primary 为 #e67e22,而非 #3498db2.2 嵌套与 & 父选择器
Sass 嵌套大幅减少重复书写父选择器。
.nav {
display: flex;
list-style: none;
padding: 0;
// 嵌套子元素
&-item { // 生成 .nav-item
margin: 0 8px;
}
&-link { // 生成 .nav-link
text-decoration: none;
color: #333;
padding: 8px 16px;
// & 引用当前父选择器
&:hover {
color: $primary;
}
// 相邻父选择器
&::after {
content: '';
display: block;
}
// 与其他选择器组合
.is-active & { // 生成 .is-active .nav-link
font-weight: bold;
}
}
// 嵌套媒体查询(Bubble Up)
@media (max-width: 768px) {
flex-direction: column;
&-link {
padding: 12px;
}
}
}
// 编译后:
// .nav { display: flex; ... }
// .nav-item { margin: 0 8px; }
// .nav-link { text-decoration: none; ... }
// .nav-link:hover { color: #3498db; }
// .nav-link::after { content: ''; ... }
// .is-active .nav-link { font-weight: bold; }
// @media (max-width: 768px) { .nav { flex-direction: column; } .nav-link { padding: 12px; } }嵌套属性:
.card {
// 嵌套属性的命名空间
font: {
family: 'Inter', sans-serif;
size: 16px;
weight: 600;
}
margin: {
top: 10px;
bottom: 20px;
}
background: {
color: #f5f5f5;
image: url('bg.png');
size: cover;
}
}
// 编译后:
// .card { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 600; margin-top: 10px; margin-bottom: 20px; background-color: #f5f5f5; background-image: url('bg.png'); background-size: cover; }2.3 混合(Mixins)
@mixin / @include 是 Sass 最强大的功能之一,用于定义可复用的样式块。
无参数混入:
@mixin clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}
@mixin visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.container {
@include clearfix;
}
.sr-only {
@include visually-hidden;
}带参数混入:
@mixin flex-center($direction: row, $gap: 0) {
display: flex;
align-items: center;
justify-content: center;
flex-direction: $direction;
@if $gap > 0 {
gap: $gap;
}
}
@mixin responsive-font(
$min-size: 14px,
$max-size: 24px,
$min-viewport: 320px,
$max-viewport: 1200px
) {
font-size: clamp(
#{$min-size},
calc(#{$min-size} + (#{$max-size} - #{$min-size}) * ((100vw - #{$min-viewport}) / (#{$max-viewport} - #{$min-viewport}))),
#{$max-size}
);
}
@mixin box-shadow($level: 1) {
@if $level == 1 {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08);
} @else if $level == 2 {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.12);
} @else if $level == 3 {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.15);
}
}
// 使用
.header {
@include flex-center(row, 16px);
@include box-shadow(2);
}
.hero-title {
@include responsive-font(28px, 64px);
}@content 占位符——块内容传递:
@content 允许混入接收调用者传入的额外样式块,实现类似「模板方法」模式。
@mixin breakpoint($size) {
$breakpoints: (
sm: 640px,
md: 768px,
lg: 1024px,
xl: 1280px,
);
@if map-has-key($breakpoints, $size) {
$value: map-get($breakpoints, $size);
@media (min-width: $value) {
@content;
}
} @else {
@warn "Unknown breakpoint '#{$size}'.";
}
}
@mixin hover-focus {
&:hover,
&:focus-visible {
@content;
}
}
@mixin when-dark {
[data-theme='dark'] & {
@content;
}
}
// 使用 @content
.card {
background: #fff;
transition: background 0.3s, transform 0.2s;
@include hover-focus {
transform: translateY(-2px);
}
@include breakpoint(md) {
display: grid;
grid-template-columns: 1fr 1fr;
}
@include breakpoint(lg) {
grid-template-columns: 1fr 1fr 1fr;
}
@include when-dark {
background: #1a1a2e;
border-color: #333;
}
}2.4 函数(Functions)
Sass 函数与混入类似,但返回值而非样式块。
自定义函数:
// 颜色处理
@function tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
@function shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
// 间距计算
@function spacing($multiplier: 1) {
$base: 8px;
@return $base * $multiplier;
}
// Z-index 管理
@function z($layer) {
$layers: (
dropdown: 1000,
sticky: 1020,
modal: 1030,
toast: 1040,
tooltip: 1050,
);
@if map-has-key($layers, $layer) {
@return map-get($layers, $layer);
}
@warn "Unknown layer '#{$layer}'.";
@return 0;
}
// 使用
.button-primary {
background: tint($primary, 80%);
&:hover {
background: shade($primary, 10%);
}
padding: spacing(2) spacing(4); // 16px 32px
z-index: z(modal); // 1030
}常用内置函数速查表:
| 函数 | 作用 | 示例 |
|---|---|---|
darken($color, $amount) |
变暗 | darken(#3498db, 10%) |
lighten($color, $amount) |
变亮 | lighten(#3498db, 10%) |
saturate($color, $amount) |
增加饱和度 | saturate(#ccc, 30%) |
desaturate($color, $amount) |
降低饱和度 | desaturate(#f00, 20%) |
rgba($color, $alpha) |
设置透明度 | rgba(#3498db, 0.5) |
mix($a, $b, $weight) |
混合颜色 | mix(red, blue, 50%) |
map-get($map, $key) |
取映射值 | map-get($breakpoints, md) |
map-has-key($map, $key) |
键存在? | map-has-key($breakpoints, xl) |
nth($list, $n) |
获取列表第 n 项 | nth(10px 20px 30px, 2) |
str-length($str) |
字符串长度 | str-length("hello") |
unit($num) |
获取单位 | unit(10px) → px |
unitless($num) |
无单位? | unitless(10) → true |
percentage($num) |
转百分比 | percentage(0.5) → 50% |
2.5 继承:@extend 与占位符选择器
@extend 允许一个选择器继承另一个选择器的样式。与混入不同,@extend 将选择器合并,生成更精简的 CSS。
// 基础样式
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
@extend .btn;
background: #3498db;
color: #fff;
&:hover {
background: darken(#3498db, 10%);
}
}
.btn-danger {
@extend .btn;
background: #e74c3c;
color: #fff;
}
// 编译后:
// .btn, .btn-primary, .btn-danger { display: inline-flex; ... }
// .btn-primary { background: #3498db; ... }
// .btn-primary:hover { background: ... }
// .btn-danger { background: #e74c3c; ... }占位符选择器 %:
占位符不会生成到 CSS 中,只有 @extend 它的选择器才会被编译。这是定义抽象基类的最佳方式。
// 定义占位符(不会出现在 CSS 中)
%text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
%flex-center {
display: flex;
align-items: center;
justify-content: center;
}
%card-base {
background: #fff;
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 16px;
}
// 继承占位符
.card-header {
@extend %card-base;
@extend %flex-center;
border-bottom: none;
border-radius: 8px 8px 0 0;
}
.card-body {
@extend %card-base;
border-top: none;
border-radius: 0 0 8px 8px;
}
.product-title {
@extend %text-truncate;
max-width: 200px;
font-size: 16px;
}
// 编译后(没有 %text-truncate 或 %card-base 等孤立的选择器):
// .card-header, .card-body { background: #fff; border: 1px solid #e1e8ed; border-radius: 8px; padding: 16px; }
// .card-header { display: flex; align-items: center; justify-content: center; ... }
// ...@extend 注意⚠️:
- 不要在嵌套很深的规则中使用
@extend,可能导致不可预期的选择器组合 - 不要
@extend不在同一组件内的选择器,违反样式隔离原则 - 对于带参数的复用,使用
@mixin而非@extend - 推荐:只对占位符选择器使用
@extend
2.6 控制指令
Sass 提供了编程语言级别的控制流指令。
@if / @else if / @else:
@mixin button-variant($variant: 'primary') {
$bg: null;
$color: #fff;
@if $variant == 'primary' {
$bg: #3498db;
} @else if $variant == 'success' {
$bg: #2ecc71;
} @else if $variant == 'warning' {
$bg: #f39c12;
$color: #333;
} @else if $variant == 'danger' {
$bg: #e74c3c;
} @else {
$bg: #95a5a6;
}
background: $bg;
color: $color;
&:hover {
background: darken($bg, 10%);
}
}@each —— 遍历列表或映射:
// 遍历列表
$spacing-scales: xs, sm, md, lg, xl;
$spacing-values: (
xs: 4px,
sm: 8px,
md: 16px,
lg: 24px,
xl: 32px,
);
@each $size in $spacing-scales {
.p-#{$size} {
padding: map-get($spacing-values, $size);
}
.m-#{$size} {
margin: map-get($spacing-values, $size);
}
}
// 遍历映射
$theme-colors: (
primary: #3498db,
secondary: #95a5a6,
success: #2ecc71,
danger: #e74c3c,
warning: #f39c12,
info: #1abc9c,
);
@each $name, $color in $theme-colors {
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background-color: $color;
}
.btn-#{$name} {
@include button-variant($name);
}
}@for —— 数值循环:
// 网格系统
$columns: 12;
@for $i from 1 through $columns {
.col-#{$i} {
flex: 0 0 calc(#{$i} / #{$columns} * 100%);
max-width: calc(#{$i} / #{$columns} * 100%);
}
}
// 字体尺寸
@for $i from 1 through 6 {
h#{$i} {
font-size: 2rem - ($i - 1) * 0.25rem;
}
}
// @for ... to 不包含终值
@for $i from 1 to 4 {
.mt-#{$i} { margin-top: $i * 4px; }
}
// 生成 .mt-1, .mt-2, .mt-3(没有 .mt-4)@while —— 条件循环:
// 不太常用,但在某些算法中有用
$i: 1;
$scale: 12px;
@while $i <= 5 {
.fs-#{$i} {
font-size: $scale * $i;
}
$i: $i + 1;
}
// 生成 .fs-1 { font-size: 12px; } ... .fs-5 { font-size: 60px; }2.7 模块化:@use 与 @forward
Sass 的现代模块系统(从 Dart Sass 1.23.0 起)替代了旧的 @import。
@use —— 加载模块:
// _variables.scss
$primary: #3498db !default;
$font-stack: 'Inter', sans-serif !default;
$border-radius: 8px !default;
@mixin card-base {
border-radius: $border-radius;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
// _buttons.scss
@use 'variables' as v;
.btn {
background: v.$primary;
font-family: v.$font-stack;
border-radius: v.$border-radius;
}
// 命名空间别名
@use 'variables' as var;
.btn-primary {
background: var.$primary;
}
// 不加 as 时使用文件名作为命名空间
@use 'variables';
// 访问:variables.$primary
// 去除命名空间(谨慎使用,避免冲突)
@use 'variables' as *;
.btn {
background: $primary;
}@forward —— 转发模块:
@forward 将一个模块的成员"透传"给后续的 @use,常用于入口文件。
// _index.scss — 统一出口
@forward 'variables';
@forward 'mixins';
@forward 'functions';
// app.scss — 入口文件
@use 'index' as *;
// 现在可以直接使用 _variables.scss、_mixins.scss 和 _functions.scss 中的所有成员
// 无需再单独 @use 它们
// 选择性转发
// _internal.scss
$-private-var: 42; // 私有(不会被转发)
$public-var: 100; // 公开
@forward 'internal' show $public-var;
// 只公开 $public-var,隐藏 $-private-var
// 转发表
@forward 'colors' as color-*;
// 将 colors 模块中所有成员重命名为 color-* 前缀可见性控制:
// 私有成员(以 - 或 _ 开头,不会被 @use 外部访问)
$-internal-padding: 16px;
$_internal-mixin {
// ...
}
// 公有成员
$public-primary: #3498db;2.8 项目组织:7-1 模式
Sass 7-1 模式是业界最广泛使用的组织方式,适用于中大型项目。
styles/
│
├── abstracts/ # 抽象层(不生成 CSS)
│ ├── _variables.scss # 变量
│ ├── _functions.scss # 函数
│ ├── _mixins.scss # 混入
│ └── _placeholders.scss # 占位符
│
├── vendors/ # 第三方库
│ ├── _bootstrap.scss
│ └── _font-awesome.scss
│
├── base/ # 基础样式
│ ├── _reset.scss # 重置
│ ├── _typography.scss # 排版
│ └── _animations.scss # 动画
│
├── components/ # 组件样式
│ ├── _buttons.scss
│ ├── _cards.scss
│ ├── _forms.scss
│ ├── _nav.scss
│ └── _modals.scss
│
├── layout/ # 布局
│ ├── _header.scss
│ ├── _footer.scss
│ ├── _sidebar.scss
│ └── _grid.scss
│
├── pages/ # 页面专用样式
│ ├── _home.scss
│ └── _about.scss
│
└── main.scss # 入口文件(仅 @use)入口文件 main.scss:
// 1. 依赖
@use 'abstracts/variables';
@use 'abstracts/functions';
@use 'abstracts/mixins';
@use 'abstracts/placeholders';
// 2. 第三方
@use 'vendors/bootstrap';
@use 'vendors/font-awesome';
// 3. 基础
@use 'base/reset';
@use 'base/typography';
@use 'base/animations';
// 4. 布局
@use 'layout/header';
@use 'layout/footer';
@use 'layout/sidebar';
@use 'layout/grid';
// 5. 组件
@use 'components/buttons';
@use 'components/cards';
@use 'components/forms';
@use 'components/nav';
@use 'components/modals';
// 6. 页面
@use 'pages/home';
@use 'pages/about';3. Less 对比学习
Less(Leaner Style Sheets)是另一款流行的 CSS 预处理器,以 JavaScript 实现,可以在 Node.js 或浏览器中运行。
3.1 变量与混入
变量:
// Less 使用 @ 定义变量
@primary: #3498db;
@secondary: #2ecc71;
@font-stack: 'Inter', Arial, sans-serif;
@base-padding: 16px;
@grid-columns: 12;
.button {
color: @primary;
font-family: @font-stack;
padding: @base-padding;
}
// 懒加载(Lazy Loading)—— Less 的重要特性
@size: 20px;
.header {
font-size: @size; // 24px(使用后面定义的值)
}
@size: 24px;
.footer {
font-size: @size; // 24px
}
// Sass 中则是顺序解析,先定义的先使用混入(Mixin)—— Less 无 @mixin 语法,选择器可直接作为混入:
// 定义混入(类似 .class)
.border-radius(@radius: 4px) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
border-radius: @radius;
}
.box-shadow(@x: 0, @y: 2px, @blur: 4px, @color: rgba(0,0,0,0.1)) {
-webkit-box-shadow: @arguments;
box-shadow: @arguments;
}
// 命名参数(与顺序无关)
.card {
.border-radius(8px);
.box-shadow(@blur: 8px, @color: rgba(0,0,0,0.2));
background: #fff;
padding: 16px;
}
// 混入集合
#utils {
.text-truncate() {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.clearfix() {
&::after {
content: '';
display: table;
clear: both;
}
}
}
.title {
#utils.text-truncate();
max-width: 200px;
}3.2 嵌套与 &
与 Sass 非常相似:
.nav {
display: flex;
list-style: none;
> li {
margin: 0 8px;
}
a {
text-decoration: none;
color: @primary;
&:hover {
color: darken(@primary, 10%);
}
// 父选择器拼接
&-link {
padding: 8px;
}
}
// 作用域内定义的变量
@nav-padding: 16px;
padding: @nav-padding;
}3.3 Sass vs Less 核心差异
| 特性 | Sass/SCSS | Less |
|---|---|---|
| 实现语言 | Dart(原生)/ LibSass(已弃用) | JavaScript |
| 变量前缀 | $ |
@ |
| 变量作用域 | 块作用域(先声明后使用) | 块作用域(懒加载) |
| 混入语法 | @mixin / @include |
.class() 直接复用 |
| 条件 | @if / @else |
混合了 guard(when) |
| 循环 | @each / @for / @while |
each() 函数 |
| 函数 | @function |
.mixin() 带返回值(Less 无函数关键词) |
| 自定义函数 | ✅ 原生支持 | ❌ 依赖插件 |
| 模块系统 | @use / @forward(现代) |
@import(旧)、@import (reference) |
| 生态 | 极成熟(Bootstrap 5 等) | 成熟(Bootstrap 3/4,Ant Design) |
| 编译速度 | Dart Sass 较快 | Node 环境下原生速度 |
| Map/列表操作 | 丰富(map-get, nth 等) | 有限(each + lists) |
Less 的条件守卫(Guarded Mixins):
// Less 使用 when 实现条件
.mixin(@size) when (@size > 10px) {
font-size: @size;
line-height: 1.5;
}
.mixin(@size) when (@size <= 10px) {
font-size: @size;
line-height: 1.2;
}
.text { .mixin(16px); } // 第一个 mixin 生效
// 类型守卫
.color-mixin(@color) when (iscolor(@color)) {
border-color: @color;
}
.color-mixin(@shadow) when (isruleset(@shadow)) {
box-shadow: @shadow;
}Less 的 @import 与命名空间:
// _variables.less
@primary: #3498db;
@danger: #e74c3c;
// main.less
@import 'variables';
// Less 可以通过包围来模拟命名空间
#namespace() {
.button() {
background: @primary;
color: #fff;
}
}
.btn {
#namespace.button(); // 避免命名冲突
}4. PostCSS 生态
PostCSS 不是预处理器,而是一个用 JavaScript 插件处理 CSS 的后处理工具链。它的核心理念是:你写标准 CSS,插件自动完成兼容性、优化、未来语法转换等工作。
4.1 核心插件
autoprefixer —— 自动添加厂商前缀:
/* 输入 */
.grid {
display: grid;
user-select: none;
appearance: none;
backdrop-filter: blur(10px);
}
/* 输出(含 autoprefixer) */
.grid {
display: -ms-grid;
display: grid;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}配置(browserslist):
// package.json
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not op_mini all"
]
}// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')({
// 也可以在这里覆盖 browserslist
overrideBrowserslist: ['last 2 versions', '> 1%'],
}),
],
};cssnano —— 极致压缩:
/* 输入 */
.card {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 24px;
margin: 8px 0;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 16px;
line-height: 1.5;
color: #333333;
}
/* 输出(cssnano 压缩后) */
.card {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 24px;
margin: 8px 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font: 16px/1.5 sans-serif;
color: #333;
}cssnano 的压缩类型:
| preset | 特点 | 适合场景 |
|---|---|---|
cssnano-preset-default |
安全压缩,不改变 CSS 语义 | 生产环境(推荐) |
cssnano-preset-advanced |
激进压缩(可能破坏某些模式) | 绝对确定语义时 |
cssnano-preset-lite |
轻量压缩,速度最快 | 开发环境检查 |
postcss-preset-env —— 未来 CSS 语法转换:
将未来的 CSS 特性(Stage 0-4)转换为当前浏览器兼容的代码。
/* 输入:未来 CSS */
:root {
--primary: #3498db;
--gap: clamp(1rem, 4vw, 3rem);
}
.card {
background: oklch(70% 0.15 250deg);
color: color-mix(in srgb, var(--primary), white 30%);
container-type: inline-size;
}
@custom-media --viewport-md (width >= 768px);
@media (--viewport-md) {
.grid {
--col-count: 3;
}
}
/* 输出(postcss-preset-env stage 0) */
:root {
--primary: #3498db;
--gap: clamp(1rem, 4vw, 3rem);
}
.card {
/* oklch → rgb 转换 */
background: rgb(51, 139, 214);
color: rgb(117, 189, 232); /* color-mix 展开 */
container-type: inline-size;
}
/* @custom-media → 标准媒体查询 */
@media (min-width: 768px) {
.grid {
--col-count: 3;
}
}// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 2, // 默认 stage 2,生产推荐
browsers: 'last 2 versions',
autoprefixer: true, // 内置 autoprefixer
features: {
'custom-properties': false, // 不编译 CSS 变量(保留给浏览器)
'nesting-rules': true, // 启用嵌套
'custom-media-queries': true,
'gap-properties': true,
},
}),
],
};PostCSS 嵌套 vs Sass 嵌套:
/* PostCSS 嵌套(postcss-nesting 或 postcss-preset-env) */
.card {
background: #fff;
& .title {
font-size: 18px;
}
&:hover {
transform: translateY(-2px);
}
@media (width >= 768px) {
padding: 24px;
}
}4.2 完整 PostCSS 工作流
// postcss.config.js — 生产环境配置
module.exports = {
plugins: [
// 1. 解析未来 CSS
require('postcss-preset-env')({
stage: 2,
features: {
'nesting-rules': true,
'custom-media-queries': true,
},
}),
// 2. 其他后处理
require('postcss-import'), // 合并 @import
require('postcss-assets')({ // 资源处理(图片尺寸等)
loadPaths: ['src/images/'],
}),
// 3. 压缩(仅生产环境)
...(process.env.NODE_ENV === 'production'
? [require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
}],
})]
: []),
],
};4.3 与预处理器的配合
PostCSS 通常不替代预处理器,而是在预处理器之后运行。
源文件(SCSS) → Sass 编译 → CSS → PostCSS 处理 → 最终 CSS
↓
autoprefixer
cssnano
postcss-preset-env构建配置示例(Webpack):
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 3. 注入到 DOM
'css-loader', // 2. 解析 CSS
{
loader: 'postcss-loader', // 1b. PostCSS 后处理
options: {
postcssOptions: {
plugins: [
'autoprefixer',
process.env.NODE_ENV === 'production' && 'cssnano',
].filter(Boolean),
},
},
},
'sass-loader', // 1a. Sass 编译
],
},
],
},
};5. 预处理器 vs CSS 变量 选型指南
5.1 使用场景对比表
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 设计系统令牌(颜色、字体、间距) | CSS 变量 | 易于在运行时更新,支持暗色模式 |
| 动态主题切换 | CSS 变量 + JS | 运行时修改,无刷新即时生效 |
| 复杂计算(颜色运算、数学公式) | 预处理器 | 内置 darken/lighten 等函数 |
| 响应式设计 | CSS 变量 | 可在媒体查询中改变值 |
| 代码复用(Mixin) | 预处理器 | @mixin / @include 无可替代 |
| 循环生成(网格、工具类) | 预处理器 | @for / @each |
| 组件级别的主题覆盖 | CSS 变量 | 作用域继承,组件实例化 |
| 向后兼容(旧浏览器) | 预处理器 | CSS 变量需 postcss-preset-env polyfill |
| 性能敏感场景 | 预处理器 | 编译时确定,无运行时开销 |
| 动画 + 变量 | CSS 变量 + @property |
支持过渡和关键帧动画 |
| 嵌套选择器 | 预处理器或 PostCSS | 减少重复 |
| 组织大型项目 | 预处理器 + 7-1 模式 | 模块化系统成熟 |
5.2 同时使用的最佳实践
CSS 变量与预处理器不是二选一,而是互补工具。 最佳方案是同时使用二者,各取所长。
原则 1:预处理器负责编译时任务,CSS 变量负责运行时任务
// _variables.scss — Sass 变量(编译时)
$color-primary: #3498db;
$color-success: #2ecc71;
$color-danger: #e74c3c;
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 16px;
// _tokens.scss — CSS 变量(运行时)
:root {
--color-primary: #{$color-primary};
--color-success: #{$color-success};
--color-danger: #{$color-danger};
--border-radius-sm: #{$border-radius-sm};
--border-radius-md: #{$border-radius-md};
--border-radius-lg: #{$border-radius-lg};
// 计算后的值
--spacing-xs: #{spacing(0.5)};
--spacing-sm: #{spacing(1)};
--spacing-md: #{spacing(2)};
--spacing-lg: #{spacing(4)};
--spacing-xl: #{spacing(8)};
}原则 2:预处理器做静态生成,CSS 变量做动态覆盖
// 按钮组件
@mixin button-base {
display: inline-flex;
align-items: center;
gap: var(--btn-gap, 8px);
padding: var(--btn-padding-y, 10px) var(--btn-padding-x, 20px);
font-size: var(--btn-font-size, 14px);
border-radius: var(--btn-radius, var(--border-radius-md, 6px));
border: var(--btn-border, none);
cursor: pointer;
transition: all 0.2s;
}
@mixin button-variant($name, $bg, $color: #fff) {
.btn-#{$name} {
@include button-base;
--btn-bg: #{$bg};
--btn-color: #{$color};
--btn-hover-bg: #{darken($bg, 8%)};
background: var(--btn-bg);
color: var(--btn-color);
&:hover {
background: var(--btn-hover-bg);
}
}
}
// 生成多个按钮变体
$variants: (
primary: (#3498db),
success: (#2ecc71),
danger: (#e74c3c),
warning: (#f39c12, #333),
outline: (transparent, #333),
);
@each $name, $params in $variants {
@include button-variant($name, $params...);
}
// 使用时动态覆盖
// <button class="btn-primary" style="--btn-radius: 0; --btn-font-size: 16px;">动态按钮</button>原则 3:Sass 生成实用工具类,CSS 变量控制细节
// spacing 工具类生成
$spacing-directions: (
't': 'top',
'r': 'right',
'b': 'bottom',
'l': 'left',
);
$spacing-sizes: (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64);
@each $size in $spacing-sizes {
// padding
.p-#{$size} { padding: calc(var(--spacing-unit, 4px) * #{$size}); }
.px-#{$size} {
padding-left: calc(var(--spacing-unit, 4px) * #{$size});
padding-right: calc(var(--spacing-unit, 4px) * #{$size});
}
.py-#{$size} {
padding-top: calc(var(--spacing-unit, 4px) * #{$size});
padding-bottom: calc(var(--spacing-unit, 4px) * #{$size});
}
// margin
.m-#{$size} { margin: calc(var(--spacing-unit, 4px) * #{$size}); }
// gap
.gap-#{$size} { gap: calc(var(--spacing-unit, 4px) * #{$size}); }
// 方向性
@each $dir, $direction in $spacing-directions {
.p#{$dir}-#{$size} {
padding-#{$direction}: calc(var(--spacing-unit, 4px) * #{$size});
}
.m#{$dir}-#{$size} {
margin-#{$direction}: calc(var(--spacing-unit, 4px) * #{$size});
}
}
}5.3 最终决策框架
面对一个新项目,需要根据项目特性选择工具:
┌──────────────────────────────┐
│ 项目启动 │
└──────┬───────────────────────┘
│
▼
┌──────────────────────────────┐
│ 需要动态主题/运行时修改? │
├──────┬───────────────────────┤
│ 是 │ → 必须使用 CSS 变量 │
│ 否 │ → 可以仅用预处理器 │
└──────┴───────────────────────┘
│
▼
┌──────────────────────────────┐
│ 项目规模? │
├──────┬───────────────────────┤
│ 小型 │ → CSS 变量 + PostCSS │
│ 中型 │ → CSS 变量 + Sass │
│ 大型 │ → CSS 变量 + Sass │
│ │ 7-1 模式 + PostCSS │
└──────┴───────────────────────┘
│
▼
┌──────────────────────────────┐
│ 团队熟悉度? │
├──────┬───────────────────────┤
│ 偏 JS 团队 │ → Less + │
│ │ PostCSS │
│ 偏传统前端 │ → SCSS + │
│ │ PostCSS │
│ 追求未来 │ → PostCSS │
│ │ (preset-env) │
└─────────────┴───────────────┘推荐组合(2024+):
| 场景 | 推荐方案 |
|---|---|
| 新项目(标准) | Sass (Dart Sass) + PostCSS + CSS 变量 |
| 新项目(激进) | PostCSS-preset-env + CSS 变量 + @property |
| 组件库 / 设计系统 | Sass (编译时生成) + CSS 变量 (运行时主题) + PostCSS (兼容) |
| 遗留项目迁移 | 保留现有预处理器,逐步引入 CSS 变量 |
| 演示 / 原型 | 纯 CSS 变量 + CDN PostCSS |
清单:检查你同时使用预处理器和 CSS 变量时是否正确:
- [ ] Sass 变量用于编译时的循环、生成、计算
- [ ] CSS 变量用于运行时的主题、覆盖、响应式
- [ ] 用
#{$var}插值将 Sass 变量的值注入 CSS 变量 - [ ] Sass Mixin 生成模板代码,CSS 变量控制实例化细节
- [ ] 颜色等复杂运算在 Sass 中完成,结果以 CSS 变量暴露
- [ ] 组件库提供 Sass API(
$primary !default)和 CSS API(--primary) - [ ] PostCSS 放在构建管道的最后一步进行兼容和优化
结语
CSS 自定义属性和预处理器的关系不是替代,而是互补。预处理器擅长编译时的代码生成、循环、计算和组织,CSS 变量擅长运行时的动态更新、继承和主题系统。
用预处理器构建系统的骨架,用 CSS 变量赋予系统生命。 当你能熟练地在 Sass 中 @each 生成工具类,同时用 CSS 变量让用户在运行时切换主题时,你才真正掌握了现代 CSS 工程化的精髓。
未来的方向是原生 CSS 越来越强大——@property、@layer、nesting、container queries 都在缩小与预处理器的差距。但就目前而言,Sass/SCSS + CSS 变量 + PostCSS 的组合仍然是生产项目最成熟、最平衡的选择。
进一步阅读:
- CSS Custom Properties Specification (CSSWG)
- Sass Documentation
- PostCSS Preset Env
- MDN: Using CSS custom properties
- CSS @property (Houdini)
本文发布于 2024-08-30,文中涉及的技术版本以撰写时为准。CSS 和 Sass 均在积极演进中,建议关注各项目官方文档获取最新信息。