iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 13 篇

【Day 13】主題樣式:先利其器,認識 SCSS

  • 分享至 

  • xImage
  •  

昨天拆解了 Bootstrap 的主題色設定:只要把新顏色加進 $theme-colors 這個 map,就能自動產生 --bs-* 變數和 .btn-*、.alert-* 等 class。

動手寫主題檔之前,今天先整理這背後會用到的 SCSS 語法。

Sass / SCSS 是什麼

Sass 是一種 CSS 預處理器。瀏覽器看不懂 .scss 或 .sass 檔案,必須先編譯成 CSS 才能使用。Sass 有兩種寫法:

  1. .scss:用 {} 和 ;,語法是 CSS 的超集合,直接貼 CSS 進去也能編譯。
  2. .sass:用縮排取代大括號,也不寫分號。

兩種寫法的功能相同,這系列都用 .scss。

💡 本篇以 Dart Sass 1.80 以後的模組化寫法(@use)為主。


變數與插值

變數

用 $ 宣告變數,把重複使用的顏色、尺寸抽出來管理。

$primary: #3498db;

:root { --primary: #3498db; }

.a { color: $primary; }
.b { color: var(--primary); }

編譯後:

:root { --primary: #3498db; }
.a { color: #3498db; }         /* $primary 被換成實際的值,變數本身消失 */
.b { color: var(--primary); }  /* 原樣保留,交給瀏覽器處理 */

SCSS 變數 vs. CSS 變數

是否需經過編譯,讓這兩種變數有不一樣的特性:

項目 SCSS 變數 $var CSS 變數 --var
生效時間 編譯時 瀏覽器執行時
編譯後 變成固定值,變數本身消失 原樣保留在 CSS 中
作用範圍 依 {} 區塊(詞法作用域) 依 DOM 結構繼承
執行時覆寫 ❌ 編譯後固定 ✅ 可用選擇器或 JS 覆寫
用在選擇器、media query、屬性名 ✅ 搭配 #{} 插值 ❌ 只能用在屬性值
Sass 函式運算 ✅ ❌ 編譯時拿不到值,只能交給瀏覽器的 calc()

所以主題會兩個一起用:用 SCSS 管理「原始色票」,再輸出成 CSS 變數,讓畫面能在執行時切換。

插值 #{}

#{} 能把變數的值「插」進選擇器、屬性名或字串中。主題檔會大量使用到這個語法,因為 class 名稱和 CSS 變數名稱都是組出來的。

$name: detective;
$key: flip-bg;
$color: #fcefbd;

.theme-#{$name} {
  --theme-#{$key}: #{$color};
  --wrong: $color;
}

編譯後:

.theme-detective {
  --theme-flip-bg: #fcefbd;
  --wrong: $color;
}

💡 CSS 變數的值一定要用 #{} 包起來。Sass 會把 --* 後面的內容當成字串原樣輸出,不會代換變數。


巢狀與 &

SCSS 可以把子選擇器寫在父選擇器裡面。& 代表「父選擇器本身」,常用來接偽類或疊加 class。

.btn {
  color: #333;
  &:hover { color: #000; }        // .btn:hover
  &.active { font-weight: 700; }  // .btn.active(同一個元素)
  .icon { width: 16px; }          // .btn .icon(子元素)
}

Map 與 sass:map

宣告

Map 就是 key: value 的集合,用小括號包起來,每組之間用逗號分隔。map 名稱慣例上用複數。

$breakpoints: (
  sm: 576px,
  md: 768px,
  lg: 992px,
);

// 能寫成巢狀:色系 → 色階 → 色碼
$colors: (
  primary: (
    100: #ffe9d2,
    500: #ff7300,
  ),
  accent: #b1926c,
);

讀取與操作

宣告完之後,要用 sass:map 模組的函式來讀取:

@use "sass:map";

map.get($colors, accent);             // #b1926c
map.get($colors, primary, 500);       // #ff7300(巢狀 map 可以一路往下找)
map.has-key($colors, primary, 900);   // false(沒有 900 這個色階)
map.merge($colors, (accent: #806542)) // 回傳合併後的新 map,同名 key 以後者為準

💡 Day 12 提到 Bootstrap 使用的 map-merge() 。map-merge() 是舊版的全域函式,模組化之後的寫法就是 map.merge(),功能相同。


@use 與 @forward

@use:載入模組

@use 用來載入 Sass 內建模組(sass:map、sass:color、sass:list…)或其他 SCSS 檔案,必須寫在檔案最上方。

// _variables.scss
$primary: #3498db;

// main.scss
@use "sass:map";
@use "variables";          // 命名空間預設是檔名:variables.$primary
@use "variables" as v;     // 也能自訂縮寫:v.$primary

.title { color: v.$primary; }

💡 檔名開頭的 _ 代表這是「partial」,不會被單獨編譯成 CSS,@use 時可以省略 _ 和副檔名。

@forward:轉發模組

@forward 會把另一個檔案的變數、函式、mixin 轉發出去,常用在 _index.scss,讓使用端只要載入一次。

// styles/_index.scss
@forward "functions";
@forward "variables";

// main.scss
@use "styles";   // 會自動找 styles/_index.scss
.title { color: styles.$primary; }

為什麼不用 @import

@import 會把所有東西匯入全域,容易命名衝突,也很難追查某個變數是在哪個檔案定義的。Dart Sass 1.80 起,@import 和 map-get()、map-merge() 這類全域函式都被標示為棄用,官方預計在 Dart Sass 3.0.0 移除。


@mixin 與 @function

@mixin / @include:重複使用的樣式區塊

用 @mixin 定義樣式區塊,可以帶參數和預設值,再用 @include 套用。

@mixin flex-center($dir: row) {
  display: flex;
  flex-direction: $dir;
  justify-content: center;
  align-items: center;
}

.box { @include flex-center(column); }
.list { @include flex-center; }   // 使用預設值 row

編譯後,.box 和 .list 會各自展開一份完整的樣式,差別只在 flex-direction。

@function / @return:回傳一個值

mixin 輸出的是「一段樣式」,function 回傳的是「一個值」。

下面這個函式會把 hex 色碼轉成 r, g, b 字串。為什麼需要這種格式?因為 CSS 變數沒辦法直接調透明度,但如果變數存的是 255, 115, 0,就能寫成 rgba(var(--theme-rgb-primary-500), 0.5)。

@use "sass:color";

@function hex-to-rgb($color) {
  $red: color.channel($color, "red", $space: rgb);
  $green: color.channel($color, "green", $space: rgb);
  $blue: color.channel($color, "blue", $space: rgb);
  @return "#{$red}, #{$green}, #{$blue}";
}

// hex-to-rgb(#ff7300) → "255, 115, 0"

補充:% 佔位選擇器與 @extend

如果好幾個選擇器共用完全相同的樣式,也可以用 % 宣告佔位選擇器,再用 @extend 繼承。

%card-base {
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
}

.plan-card { @extend %card-base; }
.product-card { @extend %card-base; padding: 24px; }

編譯後:

.product-card, .plan-card {
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.product-card {
  padding: 24px;
}

兩者的差別在於:@mixin 會在每個地方複製一份樣式,但可以帶參數;@extend 會把選擇器合併在一起,輸出比較精簡,但不能帶參數,也不能跨 @media 使用。主題檔需要依主題帶入不同顏色,所以用的是 @mixin。


流程控制

@each:遍歷 List 或 Map

讀取 $theme-colors,每個顏色都產生一組 class。

$theme-colors: (
  "primary": #0d6efd,
  "success": #198754,
  "milk-tea": #a0714f,
);

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

編譯後:

.btn-primary { background-color: #0d6efd; }
.btn-success { background-color: #198754; }
.btn-milk-tea { background-color: #a0714f; }

遍歷 List 時只有一個變數:@each $step in 100, 500, 900 { ... }。

@if / @else if / @else:條件判斷

後面接條件式,依照結果決定要編譯哪一段,範例參見下方。

@error:中止編譯

@error 會中止編譯並顯示錯誤訊息,常用來檢查 mixin、function 收到的參數是否正確。

@mixin mode($mode: "light") {
  @if $mode == "dark" {
    background-color: #1a1a1a;
    color: #fff;
  } @else if $mode == "light" {
    background-color: #fff;
    color: #333;
  } @else {
    @error "不支援的模式:#{$mode}";
  }
}

.panel { @include mode("dark"); }
.card { @include mode("blue"); }   // 編譯失敗:不支援的模式:blue

.
.
.
看社群討論,Atomic CSS 是近年的主流,所以我有猶豫要不要介紹 SCSS。不過舊專案常會用到它,不少 UI 框架的底層也是 SCSS。還有一種情況,就是跟我一樣:

我看到專案有裝 Tailwind,乾ㄟ當用?> (゚∀゚  )
) < 可以啊

------ A Few Week Later ------

) < 這樣寫 class 名太長了,不好閱讀。
                 ?!(゚∀゚  )

最終,還是決定繼續寫下去。
今天整理了會用到的 SCSS 語法。明天就用這些語法,把主題色票 map 寫成實際的主題檔!


參考資料


上一篇
【Day 12】主題樣式:不只是深色模式
下一篇
【Day 14】主題樣式:把色票 map 寫成主題檔
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言