昨天拆解了 Bootstrap 的主題色設定:只要把新顏色加進 $theme-colors 這個 map,就能自動產生 --bs-* 變數和 .btn-*、.alert-* 等 class。
動手寫主題檔之前,今天先整理這背後會用到的 SCSS 語法。
Sass 是一種 CSS 預處理器。瀏覽器看不懂 .scss 或 .sass 檔案,必須先編譯成 CSS 才能使用。Sass 有兩種寫法:
.scss:用 {} 和 ;,語法是 CSS 的超集合,直接貼 CSS 進去也能編譯。.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 變數 $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(子元素)
}
sass:mapMap 就是 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 寫成實際的主題檔!