昨天介紹了 SCSS 語法,對 SCSS 有了基本的了解。今天就用這些語法寫成主題檔:透過一個色彩 map 產出主題的 CSS 變數,再搭配 theme- 前綴的 class 寫通用主題樣式。那麼我們開始吧!
@each → CSS 變數先來設定變數,設計稿上提供了 primary 與 accent 兩套色票。我們用巢狀的方式寫在 $themes 變數下:
// 等等會用到,先在檔案上方引用
@use "sass:map";
@use "functions" as fn;
$themes: (
coffee: (
primary: (100: #ffe9d2, 500: #ff7300),
accent: (100: #fff1db),
),
matcha: (
primary: (100: #ddfee7, 500: #10c659),
accent: (100: #ecffcf),
),
);
接下來,我希望在 DOM 下 .theme-container 開啟主題用色,切換主題則是用在同層加上不同 .theme-{主題名稱}。例如寫 .theme-container.theme-matcha,就能在所屬的 DOM 分支中使用主題 CSS 變數。
.theme-container {
@each $name, $theme in $themes { // 外層
&.theme-#{$name} { // 中層
@each $palette in (primary, accent) { // 內層
@each $step, $color in map.get($theme, $palette) {
--theme-#{$palette}-#{$step}: #{$color};
// 方便做半透明背景或陰影,所以從 fn 模組引用 Day13 寫的自訂色碼轉換函式
--theme-rgb-#{$palette}-#{$step}: #{fn.hex-to-rgb($color)};
}
}
}
}
}
三個 @each 由外到內,一層一層往 map 裡面取值:
| 遍歷的對象 | 參數 | 第一圈的值 | |
|---|---|---|---|
| 外層 | $themes map |
$name:主題名稱(key);$theme:該主題的色票 map(value) |
coffee;(primary: (...), accent: (...)) |
| 中層 | (primary, accent) 這個 list |
$palette:色系名稱 |
primary |
| 內層 | 該主題、該色系的色階 map | $step:色階(key);$color:色碼(value) |
100;#ffe9d2 |
編譯後:
.theme-container.theme-coffee {
--theme-primary-100: #ffe9d2;
--theme-rgb-primary-100: 255, 233, 210;
--theme-primary-500: #ff7300;
--theme-rgb-primary-500: 255, 115, 0;
--theme-accent-100: #fff1db;
--theme-rgb-accent-100: 255, 241, 219;
}
.theme-container.theme-matcha {
--theme-primary-100: #ddfee7;
--theme-rgb-primary-100: 221, 254, 231;
--theme-primary-500: #10c659;
--theme-rgb-primary-500: 16, 198, 89;
--theme-accent-100: #ecffcf;
--theme-rgb-accent-100: 236, 255, 207;
}
要新增第三個主題,只要在 $themes 裡多加一組色票就可以了。
JS 處理主題資料時,可能沒有取到主題名稱,或是名稱不合規。所以需要一組預設變數放在 .theme-container 下,找不到對應的 .theme-{主題名稱} 時,還有預設主題變數可以套用。
預設主題變數的產生方式和其他主題相同,只差在選擇器不一樣。所以我們先用 @mixin 把重複的部分(中層、內層的 @each)抽出來。
// input 主題名稱、該主題色票 map
@mixin theme-vars($name, $theme) {
@each $palette in (primary, accent) {
@each $step, $color in map.get($theme, $palette) {
--theme-#{$palette}-#{$step}: #{$color};
--theme-rgb-#{$palette}-#{$step}: #{fn.hex-to-rgb($color)};
}
}
}
接著新增預設主題的色票,結構和其他主題一樣:
$theme-default: (
primary: (100: #fee8c8, 500: #ff8c34),
accent: (100: #fdf4dd),
);
最後在 .theme-container 裡呼叫兩次:一次直接輸出預設主題,一次放在外層的 @each 裡輸出各主題。
.theme-container {
@include theme-vars(default, $theme-default);
@each $name, $theme in $themes{
&.theme-#{$name} {
@include theme-vars($name, $theme);
}
}
}
編譯後:
.theme-container {
--theme-primary-100: #fee8c8;
--theme-rgb-primary-100: 254, 232, 200;
--theme-primary-500: #ff8c34;
--theme-rgb-primary-500: 255, 140, 52;
--theme-accent-100: #fdf4dd;
--theme-rgb-accent-100: 253, 244, 221;
}
.theme-container.theme-coffee {
/* ...同上一段 */
}
.theme-container.theme-matcha {
/* ...同上一段 */
}
.theme-container.theme-matcha 的權重比 .theme-container 高,所以有主題 class 時,同名變數會被主題覆蓋;沒有主題 class,或主題名稱打錯時,就只套用預設主題。
色票一多,難免漏填。漏填的情況有兩種:
accent,編譯會失敗,但錯誤原因是 map.get 找不到 key 時會回傳 null,一路傳進 hex-to-rgb 才出錯,所以錯誤指向 _functions.scss,看不出是哪個主題、少了什麼。primary 只寫了 100。編譯會成功,.theme-container.theme-matcha 只是少了 --theme-primary-500。看不出來少了什麼色階。再加上預設主題,雖然能避免畫面壞掉,但也讓漏填變得更難發現。所以要在編譯前先檢查 map,用 @error 讓漏填直接編譯失敗。
首先,列出每個色系必填的色階:
$required-steps: (
primary: (100, 500),
accent: (100),
);
在 theme-vars 的最前面檢查一輪,順便把中層的 (primary, accent) 換成 map.keys($required-steps),之後新增色系只要改 $required-steps 一個地方:
@mixin theme-vars($name, $theme) {
// 檢查必填色階
@each $palette, $steps in $required-steps{
@each $step in $steps{
@if not map.has-key($theme, $palette, $step) {
@error "主題 #{$name} 缺少 #{$palette} #{$step}";
}
}
}
@each $palette in map.keys($required-steps) {
// 其他一樣
}
}
map.has-key($theme, $palette, $step) 會一路往下找,不管是少了整個色系,還是只少一個色階,都會回傳 false。錯誤訊息會指出主題名稱和缺少的色階:
Error: "主題 matcha 缺少 accent 100"
Error: "主題 matcha 缺少 primary 500"
依照前面的架構,將完整色彩系統更新上去後,現在我們能直接在 .theme-container 容器中,讀 var(--theme-*) 來取用主題變數。因此在寫 CSS 時不用管現在是哪個主題,統一用主題變數,就只要寫一次。
以按鈕顏色為例,CSS 只需要寫:
.theme-container {
.btn-theme {
color: #fff;
background-color: var(--theme-primary-500);
&:hover { background-color: var(--theme-primary-300); }
&:active { background-color: var(--theme-primary-700); }
&:focus { box-shadow: 0 0 5px 5px rgba(var(--theme-rgb-primary-500), .5); }
}
}
HTML 部分只需要加上不同 .theme-{主題名稱} 的 class 就行了。
<div class="theme-container theme-coffee">
<button class="btn-theme">咖啡色按鈕</button>
</div>
<div class="theme-container theme-matcha">
<button class="btn-theme">抹茶色按鈕</button>
</div>
噫!好!我完成了!
之後新增主題,就只要更新 $themes 這個 map 就行了,蒸蚌!
.
.
.
完成了……嗎?唉呦,有來自 UI/UX 的通知,來看看設計稿更新了什麼。
不只好幾個主題元件的色階用法跟其他主題的規律不同,有些甚至用了色彩系統以外的新色票。跳色設計,調皮,非常調皮。
不哭不哭,眼淚是珍珠,越哭越像豬。明天分享該如何調整現有架構,來滿足設計稿的需求。