Skip to content

Sass/SCSS 常用语法

大白话解释: Sass 就是"增强版 CSS"。原生 CSS 没有变量、没有嵌套、没有复用机制,写起来很麻烦。Sass 在 CSS 的基础上加了这些功能,让样式代码更易维护。

为什么用 Sass?

  • 变量:定义一次颜色、字号,到处复用,修改时只改一处
  • 嵌套:像 HTML 一样嵌套写样式,层级清晰
  • Mixin:把常用的样式组合打包,像函数一样复用
  • 函数:颜色计算、单位转换等,不用手写
  • 模块化:把样式拆成多个文件,最后合并成一个

Sass vs SCSS 怎么选?

  • SCSS(推荐):语法和 CSS 一样,只是加了变量、嵌套等功能,学习成本低
  • Sass(缩进语法):没有大括号和分号,更简洁,但需要适应

Sass 是最流行的 CSS 预处理器,有两种语法:SCSS(推荐,兼容 CSS)和 Sass(缩进语法)。

安装

bash
# 全局安装
npm install -g sass

# 项目安装(推荐)
npm install -D sass

# 编译
sass input.scss output.css

# 压缩编译
sass --style=compressed input.scss output.min.css

# 监听变化
sass --watch src:dist

# 监听单个文件
sass --watch input.scss:output.css

变量

基本用法

scss
// 用 $ 定义变量(Sass 用 $,Less 用 @,CSS 原生用 --)
$primary: #1890ff;        // 主色:按钮、链接等核心颜色
$success: #52c41a;        // 成功色:成功状态提示
$danger: #ff4d4f;         // 危险色:错误、删除操作
$warning: #faad14;        // 警告色:警告状态提示
$info: #1890ff;           // 信息色:一般提示

// 间距:统一管理组件内外边距,方便全局调整
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
$spacing-xxl: 48px;

// 字体:集中定义字号和字体栈,保持排版一致性
$font-size-xs: 12px;
$font-size-sm: 13px;
$font-size-base: 14px;
$font-size-lg: 16px;
$font-size-xl: 20px;
$font-size-xxl: 24px;
$font-size-h1: 32px;
$font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
$font-family-code: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;

// 圆角:按钮、卡片等组件的圆角统一管理
$border-radius-sm: 2px;
$border-radius-base: 4px;
$border-radius-lg: 8px;
$border-radius-xl: 12px;
$border-radius-round: 50%;

// 阴影:不同层级的投影效果
$box-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.09);
$box-shadow-base: 0 4px 16px rgba(0, 0, 0, 0.12);
$box-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);

// z-index 层级:统一管理层叠顺序,避免 z-index 混乱
$z-index-dropdown: 1000;
$z-index-sticky: 1020;
$z-index-fixed: 1030;
$z-index-modal-backdrop: 1040;
$z-index-modal: 1050;
$z-index-tooltip: 1060;

// 断点:响应式布局的屏幕宽度分界点
$breakpoint-sm: 576px;    // 手机横屏
$breakpoint-md: 768px;    // 平板
$breakpoint-lg: 1024px;   // 小桌面
$breakpoint-xl: 1280px;   // 大桌面
$breakpoint-xxl: 1600px;  // 超大屏

// 使用变量:直接用 $变量名 引用
.button {
  background: $primary;
  padding: $spacing-sm $spacing-md;
  font-size: $font-size-base;
  border-radius: $border-radius-base;
  box-shadow: $box-shadow-sm;
}

默认变量

scss
// 使用 !default,可以被外部覆盖
$primary: #1890ff !default;
$border-color: #ddd !default;
$border-width: 1px !default;
$border-style: solid !default;
$border-radius-base: 4px !default;

.button {
  background: $primary;
  border-radius: $border-radius-base;
}

.border {
  border: $border-width $border-style $border-color;
}

// 覆盖默认变量(必须在 @use 之前)
// 只覆盖 $primary 和 $border-radius-base,其他 !default 变量保持原值
@use './variables' with (
  $primary: #722ed1,
  $border-radius-base: 8px
);
css
/* 编译后:被覆盖的变量使用新值,未覆盖的保持 !default 默认值 */
.button {
  background: #722ed1;   /* $primary 被覆盖为 #722ed1 */
  border-radius: 8px;    /* $border-radius-base 被覆盖为 8px */
}

.border {
  border: 1px solid #ddd; /* $border-color 未被覆盖,使用 !default 值 */
}

变量插值

scss
$direction: 'top';
$property: 'margin';

.border-#{$direction} {
  border-#{$direction}: 1px solid #ddd;
}

.m-0 {
  #{$property}: 0;
}

// 在选择器中使用
$namespace: 'ant';

.#{$namespace}-btn {
  padding: 8px 16px;
}

.#{$namespace}-card {
  border: 1px solid #eee;
}
css
/* 编译后:#{} 插值被替换为变量的实际值 */
.border-top {
  border-top: 1px solid #ddd;
}

.m-0 {
  margin: 0;
}

.ant-btn {
  padding: 8px 16px;
}

.ant-card {
  border: 1px solid #eee;
}

变量作用域

scss
$color: red;

.header {
  $color: blue;  // 局部变量
  color: $color; // blue
}

.footer {
  color: $color; // red(全局变量)
}

// 使用 !global 将局部变量提升为全局
.sidebar {
  $width: 250px !global;
  width: $width;
}

.main {
  margin-left: $width; // 250px
}
css
/* 编译后:局部变量只在定义的选择器内生效,!global 提升为全局 */
.header {
  color: blue;
}

.footer {
  color: red;
}

.sidebar {
  width: 250px;
}

.main {
  margin-left: 250px;
}

嵌套

基本嵌套

scss
.nav {
  background: #fff;         // 导航栏背景色
  height: 60px;             // 导航栏高度
  
  .nav-list {
    display: flex;           // 水平排列子元素
    list-style: none;        // 去掉列表圆点
    margin: 0;
    padding: 0;
  }
  
  .nav-item {
    position: relative;      // 为子元素绝对定位提供参考
    
    &:hover {                // & 代表父选择器 .nav-item,编译后为 .nav-item:hover
      background: #f5f5f5;
    }
    
    &.active {               // 编译后为 .nav-item.active
      color: $primary;
      
      &::after {             // 伪元素:激活状态下底部的小蓝条指示器
        content: '';
        position: absolute;
        bottom: 0;
        left: 50%;
        transform: translateX(-50%);  // 水平居中
        width: 20px;
        height: 2px;
        background: $primary;
      }
    }
    
    &:first-child {          // 第一个子元素去掉左边距
      margin-left: 0;
    }
    
    &:last-child {           // 最后一个子元素去掉右边距
      margin-right: 0;
    }
  }
  
  .nav-link {
    display: block;
    padding: 0 $spacing-lg;
    line-height: 60px;       // 行高 = 容器高度,实现垂直居中
    color: #333;
    text-decoration: none;   // 去掉下划线
    transition: color 0.3s;  // 颜色过渡动画
    
    &:hover {
      color: $primary;
    }
  }
  
  // 子元素选择器:只匹配直接子代 .container
  > .container {
    display: flex;
    align-items: center;
  }
  
  // 相邻兄弟选择器:.logo 后面紧跟的 .nav-list
  .logo + .nav-list {
    margin-left: $spacing-xl;
  }
}
css
/* 编译后:嵌套展开为后代选择器链,& 替换为父选择器 */
.nav {
  background: #fff;
  height: 60px;
}
.nav .nav-list {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav .nav-item {
  position: relative;
}
.nav .nav-item:hover {
  background: #f5f5f5;
}
.nav .nav-item.active {
  color: #1890ff;
}
.nav .nav-item.active::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 20px;
  height: 2px;
  background: #1890ff;
}
.nav .nav-item:first-child {
  margin-left: 0;
}
.nav .nav-item:last-child {
  margin-right: 0;
}
.nav .nav-link {
  display: block;
  padding: 0 24px;
  line-height: 60px;
  color: #333;
  text-decoration: none;
  transition: color 0.3s;
}
.nav .nav-link:hover {
  color: #1890ff;
}
.nav > .container {
  display: flex;
  align-items: center;
}
.nav .logo + .nav-list {
  margin-left: 32px;
}

媒体查询嵌套

scss
.container {
  width: 100%;
  padding: 0 $spacing-md;
  
  @media (min-width: $breakpoint-sm) {
    max-width: 540px;
    margin: 0 auto;
  }
  
  @media (min-width: $breakpoint-md) {
    max-width: 720px;
  }
  
  @media (min-width: $breakpoint-lg) {
    max-width: 960px;
  }
  
  @media (min-width: $breakpoint-xl) {
    max-width: 1200px;
  }
  
  @media (min-width: $breakpoint-xxl) {
    max-width: 1400px;
  }
}

// 移动端优先(max-width)
.sidebar {
  width: 100%;
  display: none;
  
  @media (min-width: $breakpoint-lg) {
    width: 250px;
    display: block;
  }
}

属性嵌套

scss
.box {
  border: {
    top: 1px solid #ddd;
    right: 1px solid #ddd;
    bottom: 1px solid #ddd;
    left: 1px solid #ddd;
    radius: 4px;
  }
  
  margin: {
    top: 16px;
    bottom: 16px;
  }
  
  background: {
    color: #fff;
    image: url('bg.png');
    repeat: no-repeat;
    position: center center;
    size: cover;
  }
  
  font: {
    family: $font-family-base;
    size: $font-size-base;
    weight: 400;
  }
  line-height: 1.5;
}

Mixin(混合)

基本 Mixin

scss
// 定义 mixin:用 @mixin 关键字,名称自定义(建议用 kebab-case)
// 注意:mixin 名和变量名是独立的,不会冲突。如 $bordered(变量)和 @mixin bordered(混入)
@mixin bordered {
  border: 1px solid #ddd;
  border-radius: 4px;
}

// 为避免和变量名混淆,mixin 名建议加动词前缀,如 set-、make-、use-
@mixin set-box-shadow {
  box-shadow: $box-shadow-sm;
}

@mixin text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@mixin flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

// 使用 mixin:用 @include 关键字引入
.card {
  @include bordered;        // 展开为 border + border-radius
  @include set-box-shadow;  // 展开为 box-shadow
  padding: 16px;
  
  .title {
    @include text-ellipsis; // 单行省略号
    font-size: 16px;
  }
  
  .footer {
    @include flex-between;  // 两端对齐布局
    margin-top: 16px;
  }
}
css
/* 编译后:@mixin 定义不会输出到 CSS,@include 处展开为实际样式 */
.card {
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
  padding: 16px;
}
.card .title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 16px;
}
.card .footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16px;
}

带参数的 Mixin

scss
// 带参数
@mixin size($width, $height: $width) {
  width: $width;
  height: $height;
}

@mixin square($size) {
  @include size($size);
}

@mixin rectangle($width, $height: $width * 0.618) {
  @include size($width, $height);
}

// 使用
.avatar {
  @include square(48px);
}

.banner {
  @include rectangle(100%, 200px);
}

.icon {
  @include square(20px);
}
css
/* 编译后:参数被替换为传入的实际值,默认值在未传参时生效 */
.avatar {
  width: 48px;
  height: 48px;
}

.banner {
  width: 100%;
  height: 200px;
}

.icon {
  width: 20px;
  height: 20px;
}

默认参数

scss
@mixin padding($vertical: 8px, $horizontal: 16px) {
  padding: $vertical $horizontal;
}

.btn-sm {
  @include padding(4px, 8px);
}

.btn-md {
  @include padding();  // 使用默认值
}

.btn-lg {
  @include padding(12px, 24px);
}

// 多个默认参数
@mixin button($bg: $primary, $color: #fff, $radius: 4px) {
  background: $bg;
  color: $color;
  border-radius: $radius;
  border: none;
  cursor: pointer;
}

.btn-primary {
  @include button();
}

.btn-success {
  @include button($success);
}

.btn-outline {
  @include button(#fff, $primary, 20px);
  border: 1px solid $primary;
}

可变参数

scss
@mixin box-shadow($shadows...) {
  box-shadow: $shadows;
}

.card {
  @include box-shadow(0 2px 4px rgba(0,0,0,0.1));
}

.card-hover {
  @include box-shadow(
    0 2px 4px rgba(0,0,0,0.1),
    0 4px 12px rgba(0,0,0,0.15)
  );
}

// 渐变
@mixin gradient($direction, $colors...) {
  background: linear-gradient($direction, $colors);
}

.hero {
  @include gradient(to right, #667eea, #764ba2);
}

@content(内容块)

scss
@mixin mobile {
  @media (max-width: #{$breakpoint-md - 1px}) {
    @content;
  }
}

@mixin tablet {
  @media (min-width: $breakpoint-md) and (max-width: #{$breakpoint-lg - 1px}) {
    @content;
  }
}

@mixin desktop {
  @media (min-width: $breakpoint-lg) {
    @content;
  }
}

// 使用
.sidebar {
  width: 250px;
  
  @include mobile {
    width: 100%;
    display: none;
  }
}

.card {
  padding: 24px;
  
  @include mobile {
    padding: 16px;
  }
}

// 带参数的 @content
@mixin theme($theme) {
  @if $theme == 'light' {
    --bg: #fff;
    --text: #333;
    --border: #ddd;
    @content;
  } @else if $theme == 'dark' {
    --bg: #141414;
    --text: #fff;
    --border: #444;
    @content;
  }
}

.light-theme {
  @include theme('light') {
    background: var(--bg);
    color: var(--text);
  }
}
css
/* 编译后:@content 块被插入到 mixin 内部,响应式 mixin 将内容包裹在媒体查询中 */
.sidebar {
  width: 250px;
}
@media (max-width: 767px) {
  .sidebar {
    width: 100%;
    display: none;
  }
}

.card {
  padding: 24px;
}
@media (max-width: 767px) {
  .card {
    padding: 16px;
  }
}

.light-theme {
  --bg: #fff;
  --text: #333;
  --border: #ddd;
  background: var(--bg);
  color: var(--text);
}

占位符和继承

占位符选择器

大白话解释: 占位符 % 就像一个"模板",定义了样式但不会输出到最终 CSS 中。只有被 @extend 引用时才会生效,比 @mixin 更高效(合并选择器而非复制代码)。

什么时候用 @extend vs @mixin

  • @extend:样式相同、不需要参数时,合并选择器减少代码体积
  • @mixin:需要传参定制时,灵活但会复制代码到每个使用处
scss
// % 占位符不会输出到 CSS(没有被引用时完全消失)
%center {
  display: flex;
  justify-content: center;
  align-items: center;
}

%text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

%card-base {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 16px;
}

// 使用 @extend 继承:合并选择器,减少重复代码
.box {
  @extend %center;          // 编译后:.box 和其他继承者共享同一组样式
  width: 100px;
  height: 100px;
}

.title {
  @extend %text-ellipsis;   // 编译后:选择器被合并到 %text-ellipsis 定义处
  font-size: 16px;
}

.card {
  @extend %card-base;       // 占位符被引用后才输出
  box-shadow: $box-shadow-sm;
}
css
/* 编译后:占位符 % 不输出,@extend 会合并选择器(比 mixin 更高效) */
.box {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100px;
  height: 100px;
}

.title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 16px;
}

.card {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}

继承选择器

scss
.message {
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 16px;
}

.success {
  @extend .message;
  border-color: $success;
  color: $success;
  background: lighten($success, 40%);
}

.error {
  @extend .message;
  border-color: $danger;
  color: $danger;
  background: lighten($danger, 40%);
}

.warning {
  @extend .message;
  border-color: $warning;
  color: $warning;
  background: lighten($warning, 40%);
}
css
/* 编译后:@extend 将子类选择器合并到父类规则中 */
.message, .success, .error, .warning {
  padding: 12px 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 16px;
}

.success {
  border-color: #52c41a;
  color: #52c41a;
  background: #f6ffed;
}

.error {
  border-color: #ff4d4f;
  color: #ff4d4f;
  background: #fff2f0;
}

.warning {
  border-color: #faad14;
  color: #faad14;
  background: #fffbe6;
}

@extend vs @mixin

scss
// @extend:合并选择器,减少代码重复(适合固定样式复用)
%button-base {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary {
  @extend %button-base;     // 编译后:.btn-primary, .btn-secondary 共享基础样式
  background: $primary;
  color: #fff;
}

.btn-secondary {
  @extend %button-base;
  background: #666;
  color: #fff;
}

// 输出:.btn-primary, .btn-secondary { padding: 8px 16px; ... }

// @mixin:复制代码到每个使用处(适合需要参数定制的场景)
@mixin button($bg) {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: $bg;          // 参数使每个按钮颜色不同,所以不能用 @extend
  color: #fff;
}

.btn-primary { @include button($primary); }
.btn-secondary { @include button(#666); }

// 输出:每个选择器都有完整的样式(代码重复,但参数不同)
css
/* 编译后对比:

   @extend 合并选择器,减少重复:
   %button-base, .btn-primary, .btn-secondary {
     padding: 8px 16px;
     border: none;
     border-radius: 4px;
     cursor: pointer;
   }
   .btn-primary { background: #1890ff; color: #fff; }
   .btn-secondary { background: #666; color: #fff; }

   @mixin 复制代码到每个使用处(适合带参数):
   .btn-primary {
     padding: 8px 16px;
     border: none;
     border-radius: 4px;
     cursor: pointer;
     background: #1890ff;
     color: #fff;
   }
   .btn-secondary {
     padding: 8px 16px;
     border: none;
     border-radius: 4px;
     cursor: pointer;
     background: #666;
     color: #fff;
   }
*/

@extend 注意事项:

  1. 不要跨媒体查询继承:在 @media 内使用 @extend 引用外部占位符会报错。
  2. 选择器合并可能产生意外组合:多个 @extend 同一个占位符时,编译器会生成所有选择器的组合,CSS 文件可能变大。
  3. 不要嵌套 @extend@extend 只在编译时展开,运行时没有继承关系,嵌套调用不会产生级联效果。
  4. 优先用 % 占位符而非类选择器:用 @extend .message 继承普通类会污染 .message 的输出,%message 不会。

函数

颜色函数

scss
$base: #1890ff;

// 亮度调整
.lighter { color: lighten($base, 20%); }
.darker { color: darken($base, 20%); }

// 饱和度调整
.saturated { color: saturate($base, 20%); }
.desaturated { color: desaturate($base, 20%); }

// 色相调整
.hue-rotate { color: adjust-hue($base, 60deg); }

// 混合颜色
.mixed { color: mix($base, #ff0000, 50%); }

// 透明度
.fade { color: rgba($base, 0.5); }
.fade-in { color: opacify($base, 0.2); }
.fade-out { color: transparentize($base, 0.2); }

// 灰度
.grayscale { color: grayscale($base); }

// 对比度
.contrast { color: contrast($base, #000, #fff); }

// 互补色
.complement { color: complement($base); }

数学函数

scss
.ceil { width: ceil(1.2px); }       // 2px
.floor { width: floor(1.8px); }     // 1px
.round { width: round(1.5px); }     // 2px
.abs { width: abs(-10px); }         // 10px
.max { width: max(10px, 20px); }    // 20px
.min { width: min(10px, 20px); }    // 10px
.percentage { width: percentage(0.5); }  // 50%
.random { width: random(100) * 1px; }   // 随机值

字符串函数

scss
.to-upper { content: to-upper-case('hello'); }  // HELLO
.to-lower { content: to-lower-case('HELLO'); }  // hello
.str-length { content: str-length('hello'); }    // 5
.slice { content: str-slice('hello', 1, 3); }    // hel
.insert { content: str-insert('hello', ' world', 6); } // hello world
.index { content: str-index('hello', 'l'); }     // 3

自定义函数

scss
// 自定义函数
@function calc-rem($px, $base: 16px) {
  @return ($px / $base) * 1rem;
}

@function calc-percent($part, $whole) {
  @return percentage($part / $whole);
}

@function strip-unit($value) {
  @return $value / ($value * 0 + 1);
}

// 使用
.title {
  font-size: calc-rem(24px);  // 1.5rem
}

.col-6 {
  width: calc-percent(6, 12); // 50%
}

$spacing: 16px;
$spacing-value: strip-unit($spacing); // 16
css
/* 编译后:@function 返回值在编译期计算 */
.title {
  font-size: 1.5rem;  /* calc-rem(24px) */
}

.col-6 {
  width: 50%;          /* calc-percent(6, 12) */
}

控制指令

@if / @else if / @else

scss
@mixin theme($theme) {
  @if $theme == 'light' {
    background: #fff;
    color: #333;
    border-color: #ddd;
  } @else if $theme == 'dark' {
    background: #141414;
    color: #fff;
    border-color: #444;
  } @else {
    background: #f5f5f5;
    color: #666;
    border-color: #eee;
  }
}

.light { @include theme('light'); }
.dark { @include theme('dark'); }
.gray { @include theme('gray'); }

// 条件判断
@mixin visibility($visible) {
  @if $visible {
    display: block;
    opacity: 1;
  } @else {
    display: none;
    opacity: 0;
  }
}
css
/* 编译后:@if/@else 只输出匹配条件的分支 */
.light {
  background: #fff;
  color: #333;
  border-color: #ddd;
}

.dark {
  background: #141414;
  color: #fff;
  border-color: #444;
}

.gray {
  background: #f5f5f5;
  color: #666;
  border-color: #eee;
}

@for

scss
// from through(包含结束值)
@for $i from 1 through 5 {
  .mt-#{$i} { margin-top: $i * 8px; }
  .mb-#{$i} { margin-bottom: $i * 8px; }
  .ml-#{$i} { margin-left: $i * 8px; }
  .mr-#{$i} { margin-right: $i * 8px; }
}

// from to(不包含结束值)
@for $i from 1 to 13 {
  .col-#{$i} {
    width: percentage($i / 12);
    float: left;
    box-sizing: border-box;
    padding: 0 15px;
  }
}
css
/* 编译后:循环展开为所有生成的类(这里展示部分) */
.mt-1 { margin-top: 8px; }
.mb-1 { margin-bottom: 8px; }
.ml-1 { margin-left: 8px; }
.mr-1 { margin-right: 8px; }
.mt-2 { margin-top: 16px; }
.mb-2 { margin-bottom: 16px; }
.ml-2 { margin-left: 16px; }
.mr-2 { margin-right: 16px; }
/* ... mt-3 到 mt-5 省略 ... */

.col-1 {
  width: 8.33333%;
  float: left;
  box-sizing: border-box;
  padding: 0 15px;
}
.col-2 {
  width: 16.66667%;
  float: left;
  box-sizing: border-box;
  padding: 0 15px;
}
/* ... col-3 到 col-11 省略 ... */
.col-12 {
  width: 100%;
  float: left;
  box-sizing: border-box;
  padding: 0 15px;
}

@each

scss
// 遍历 map
$theme-colors: (
  primary: #1890ff,
  success: #52c41a,
  danger: #ff4d4f,
  warning: #faad14,
  info: #1890ff
);

@each $name, $color in $theme-colors {
  .btn-#{$name} {
    background: $color;
    color: #fff;
  }
}

// 遍历 map
$sizes: (
  sm: 12px,
  md: 14px,
  lg: 16px,
  xl: 20px
);

@each $name, $size in $sizes {
  .text-#{$name} {
    font-size: $size;
  }
}

// 多个值遍历
$icons: (
  home: '\e900',
  user: '\e901',
  settings: '\e902',
  search: '\e903'
);

@each $name, $code in $icons {
  .icon-#{$name}::before {
    content: $code;
  }
}
css
/* 编译后:@each 遍历 map 生成所有类 */
.btn-primary {
  background: #1890ff;
  color: #fff;
}
.btn-success {
  background: #52c41a;
  color: #fff;
}
.btn-danger {
  background: #ff4d4f;
  color: #fff;
}
.btn-warning {
  background: #faad14;
  color: #fff;
}
.btn-info {
  background: #1890ff;
  color: #fff;
}

.text-sm {
  font-size: 12px;
}
.text-md {
  font-size: 14px;
}
.text-lg {
  font-size: 16px;
}
.text-xl {
  font-size: 20px;
}

.icon-home::before {
  content: '\e900';
}
.icon-user::before {
  content: '\e901';
}
.icon-settings::before {
  content: '\e902';
}
.icon-search::before {
  content: '\e903';
}

@while

scss
$i: 1;
@while $i <= 5 {
  .col-#{$i} {
    width: percentage($i / 12);
  }
  $i: $i + 1;
}
css
/* 编译后:@while 循环展开为所有生成的类 */
.col-1 {
  width: 8.33333%;
}
.col-2 {
  width: 16.66667%;
}
.col-3 {
  width: 25%;
}
.col-4 {
  width: 33.33333%;
}
.col-5 {
  width: 41.66667%;
}

Maps

基本用法

scss
// 定义 map:类似 JS 中的对象,key-value 键值对
$breakpoints: (
  'sm': 576px,
  'md': 768px,
  'lg': 1024px,
  'xl': 1280px,
  'xxl': 1600px
);

$colors: (
  'primary': #1890ff,
  'success': #52c41a,
  'danger': #ff4d4f,
  'warning': #faad14,
  'info': #1890ff
);

$spacing: (
  'xs': 4px,
  'sm': 8px,
  'md': 16px,
  'lg': 24px,
  'xl': 32px,
  'xxl': 48px
);

// map 函数:按 key 获取值(类似 JS 的 obj[key])
$primary: map-get($colors, 'primary');           // #1890ff
$breakpoint-lg: map-get($breakpoints, 'lg');     // 1024px

// 检查 key 是否存在(避免报错)
@if map-has-key($colors, 'primary') {
  // 存在才执行
}

// 合并 map:把两个 map 合成一个
$more-colors: ('pink': #eb2f96, 'cyan': #13c2c2');
$all-colors: map-merge($colors, $more-colors);   // 包含所有颜色

// 获取所有 keys 和 values(用于遍历)
$color-names: map-keys($colors);                 // ('primary', 'success', ...)
$color-values: map-values($colors);              // (#1890ff, #52c41a, ...)

生成工具类

scss
// 生成颜色工具类
@each $name, $color in $colors {
  .text-#{$name} {
    color: $color;
  }
  
  .bg-#{$name} {
    background-color: $color;
  }
  
  .bg-#{$name}-light {
    background-color: rgba($color, 0.1);
  }
  
  .border-#{$name} {
    border-color: $color;
  }
}

// 生成间距工具类
@each $name, $value in $spacing {
  .m-#{$name} { margin: $value; }
  .mt-#{$name} { margin-top: $value; }
  .mr-#{$name} { margin-right: $value; }
  .mb-#{$name} { margin-bottom: $value; }
  .ml-#{$name} { margin-left: $value; }
  .mx-#{$name} { margin-left: $value; margin-right: $value; }
  .my-#{$name} { margin-top: $value; margin-bottom: $value; }
  
  .p-#{$name} { padding: $value; }
  .pt-#{$name} { padding-top: $value; }
  .pr-#{$name} { padding-right: $value; }
  .pb-#{$name} { padding-bottom: $value; }
  .pl-#{$name} { padding-left: $value; }
  .px-#{$name} { padding-left: $value; padding-right: $value; }
  .py-#{$name} { padding-top: $value; padding-bottom: $value; }
}

// 生成响应式工具类
@each $name, $breakpoint in $breakpoints {
  @media (min-width: $breakpoint) {
    .d-#{$name}-none { display: none !important; }
    .d-#{$name}-block { display: block !important; }
    .d-#{$name}-flex { display: flex !important; }
    .d-#{$name}-inline { display: inline !important; }
    .d-#{$name}-inline-block { display: inline-block !important; }
  }
}
css
/* 编译后:map 遍历 + 插值生成大量工具类(这里展示部分) */
.text-primary {
  color: #1890ff;
}
.bg-primary {
  background-color: #1890ff;
}
.bg-primary-light {
  background-color: rgba(24, 144, 255, 0.1);
}
.border-primary {
  border-color: #1890ff;
}
/* success、danger、warning、info 同理省略 */

.m-xs { margin: 4px; }
.mt-xs { margin-top: 4px; }
.mr-xs { margin-right: 4px; }
.mb-xs { margin-bottom: 4px; }
.ml-xs { margin-left: 4px; }
.mx-xs { margin-left: 4px; margin-right: 4px; }
.my-xs { margin-top: 4px; margin-bottom: 4px; }
.p-xs { padding: 4px; }
/* sm、md、lg、xl、xxl 同理省略 */

@media (min-width: 576px) {
  .d-sm-none { display: none !important; }
  .d-sm-block { display: block !important; }
  .d-sm-flex { display: flex !important; }
  .d-sm-inline { display: inline !important; }
  .d-sm-inline-block { display: inline-block !important; }
}
/* md、lg、xl、xxl 同理省略 */

Partials 和导入

文件组织

src/
  styles/
    _variables.scss      // 变量
    _functions.scss      // 函数
    _mixins.scss         // mixin
    _placeholders.scss   // 占位符
    _reset.scss          // 重置样式
    _utilities.scss      // 工具类
    _typography.scss     // 排版
    components/
      _button.scss
      _card.scss
      _form.scss
      _table.scss
      _modal.scss
    themes/
      _light.scss
      _dark.scss
    main.scss            // 主入口

@use 和 @forward(推荐)

大白话解释:

  • @use:引入模块并使用它的变量/mixin/函数,会自动加上命名空间避免冲突(类似 JS 的 import
  • @forward:把一个模块"转发"出去,自己不使用,只是作为统一出口(类似 JS 的 export
scss
// _variables.scss(定义变量的模块)
$primary: #1890ff;
$success: #52c41a;
$danger: #ff4d4f;

// _mixins.scss(定义 mixin 的模块,需要引用变量时用 @use)
@use 'variables' as vars;  // 引入变量模块,命名为 vars

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@mixin button($bg: vars.$primary) {  // 通过 命名空间.变量名 访问
  background: $bg;
  color: #fff;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
}

// main.scss(入口文件,用 @use 引入需要的模块)
@use 'variables' as vars;   // 引入变量模块
@use 'mixins' as mix;       // 引入 mixin 模块

.container {
  @include mix.flex-center;  // 命名空间.mixin名
  color: vars.$primary;      // 命名空间.变量名
}

.btn {
  @include mix.button();
}

@forward(模块导出)

@forward 的作用: 当你有一个文件夹组织了很多模块,不想让使用者逐个 @use,就用 @forward 创建一个"统一出口"。

scss
// _index.scss(统一出口:转发所有子模块,自身不使用任何变量)
@forward 'variables';    // 转发变量模块
@forward 'mixins';       // 转发 mixin 模块
@forward 'functions';    // 转发函数模块

// 使用方:只需要 @use 一次,就能访问所有子模块的内容
@use 'styles' as styles; // styles 目录下的 _index.scss

.box {
  color: styles.$primary;      // 通过 命名空间.变量名 访问 variables 中的变量
  @include styles.flex-center; // 通过 命名空间.mixin名 访问 mixins 中的 mixin
}

@use vs @forward 区别:

  • @use:自己要用,直接引入模块并访问
  • @forward:自己不用,只是把模块"转发"给外部使用者,作为统一入口

@import(旧语法,不推荐)

警告: Sass 官方已废弃 @import,将在未来版本移除。请使用 @use@forward 替代。

scss
// 旧语法(已废弃,新项目不要使用)
@import 'variables';
@import 'mixins';
@import 'reset';

// 问题:
// 1. 全局作用域,容易命名冲突(两个文件都定义 $primary 会互相覆盖)
// 2. 无法控制导出(所有变量/mixin 都暴露给全局)
// 3. 可能重复导入(同一个文件被多处 @import 会重复输出)
// 4. 无法按需加载(只能全量引入)

实际应用场景

主题系统

scss
// _theme-variables.scss
$themes: (
  light: (
    bg: #ffffff,
    bg-secondary: #f5f5f5,
    text: #333333,
    text-secondary: #666666,
    border: #dddddd,
    primary: #1890ff,
    shadow: rgba(0, 0, 0, 0.09)
  ),
  dark: (
    bg: #141414,
    bg-secondary: #1f1f1f,
    text: #ffffff,
    text-secondary: #aaaaaa,
    border: #444444,
    primary: #177ddc,
    shadow: rgba(0, 0, 0, 0.3)
  )
);

// 生成主题变量
@each $theme, $colors in $themes {
  [data-theme="#{$theme}"] {
    @each $key, $value in $colors {
      --#{$key}: #{$value};
    }
  }
}

// 使用 CSS 变量
body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--bg);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px var(--shadow);
}
css
/* 编译后:嵌套 each 循环生成 CSS 变量 */
[data-theme="light"] {
  --bg: #ffffff;
  --bg-secondary: #f5f5f5;
  --text: #333333;
  --text-secondary: #666666;
  --border: #dddddd;
  --primary: #1890ff;
  --shadow: rgba(0, 0, 0, 0.09);
}

[data-theme="dark"] {
  --bg: #141414;
  --bg-secondary: #1f1f1f;
  --text: #ffffff;
  --text-secondary: #aaaaaa;
  --border: #444444;
  --primary: #177ddc;
  --shadow: rgba(0, 0, 0, 0.3);
}

body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--bg);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px var(--shadow);
}

响应式工具类

scss
// 响应式工具 mixin
@mixin respond-to($breakpoint) {
  @if map-has-key($breakpoints, $breakpoint) {
    @media (min-width: map-get($breakpoints, $breakpoint)) {
      @content;
    }
  } @else {
    @warn "Unknown breakpoint: #{$breakpoint}";
  }
}

// 使用
.sidebar {
  display: none;
  
  @include respond-to('lg') {
    display: block;
    width: 250px;
  }
}

.content {
  padding: 16px;
  
  @include respond-to('md') {
    padding: 24px;
  }
  
  @include respond-to('lg') {
    padding: 32px;
  }
}
css
/* 编译后:respond-to mixin 根据断点名查找 map 值,生成媒体查询 */
.sidebar {
  display: none;
}
@media (min-width: 1024px) {
  .sidebar {
    display: block;
    width: 250px;
  }
}

.content {
  padding: 16px;
}
@media (min-width: 768px) {
  .content {
    padding: 24px;
  }
}
@media (min-width: 1024px) {
  .content {
    padding: 32px;
  }
}

Vue 中使用 SCSS

基础配置

vue
<template>
  <div class="container">
    <button class="btn btn-primary">按钮</button>
    <div class="card">
      <h3 class="card__title">标题</h3>
      <p class="card__desc">描述</p>
    </div>
  </div>
</template>

<style lang="scss" scoped>
@use '@/styles/variables' as vars;
@use '@/styles/mixins' as mix;

.container {
  padding: vars.$spacing-lg;
}

.btn {
  padding: vars.$spacing-sm vars.$spacing-md;
  border: none;
  border-radius: vars.$border-radius-base;
  cursor: pointer;
  
  &-primary {
    background: vars.$primary;
    color: #fff;
    
    &:hover {
      background: darken(vars.$primary, 10%);
    }
  }
}

.card {
  border: 1px solid #eee;
  border-radius: vars.$border-radius-lg;
  padding: vars.$spacing-lg;
  
  &__title {
    font-size: vars.$font-size-lg;
    margin-bottom: vars.$spacing-sm;
  }
  
  &__desc {
    color: #666;
    font-size: vars.$font-size-base;
  }
}
</style>

全局变量注入

js
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  css: {
    preprocessorOptions: {
      scss: {
        // 全局注入(Sass 新语法)
        additionalData: `@use "@/styles/variables" as *;`,
        // 旧语法
        // additionalData: `@import "@/styles/variables";`
      }
    }
  }
})

CSS Modules

vue
<template>
  <div :class="$style.container">
    <button :class="[$style.btn, $style.btnPrimary]">按钮</button>
  </div>
</template>

<style lang="scss" module>
$primary: #1890ff;

.container {
  padding: 24px;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btnPrimary {
  background: $primary;
  color: #fff;
  
  &:hover {
    background: darken($primary, 10%);
  }
}
</style>

Less vs Sass 对比

特性LessSass/SCSS
变量符号@var$var
嵌套
Mixin.mixin()@mixin + @include
继承不支持@extend
函数有限丰富(可自定义)
控制指令when + each@if@for@each@while
Maps不支持支持
模块系统@import@use + @forward
学习曲线较低较高
社区生态较小更大
推荐场景简单项目复杂项目

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准