iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Modern Web

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

【Day 14】主題樣式:把色票 map 寫成主題檔

  • 分享至 

  • xImage
  •  

昨天介紹了 SCSS 語法,對 SCSS 有了基本的了解。今天就用這些語法寫成主題檔:透過一個色彩 map 產出主題的 CSS 變數,再搭配 theme- 前綴的 class 寫通用主題樣式。那麼我們開始吧!


基礎流程:map → @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,或主題名稱打錯時,就只套用預設主題。

二、編譯前 map 檢查

色票一多,難免漏填。漏填的情況有兩種:

  1. 少了整個色系:例如 matcha 沒寫 accent,編譯會失敗,但錯誤原因是 map.get 找不到 key 時會回傳 null,一路傳進 hex-to-rgb 才出錯,所以錯誤指向 _functions.scss,看不出是哪個主題、少了什麼。
  2. 少了某個色階:例如 matcha 的 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 的通知,來看看設計稿更新了什麼。

不只好幾個主題元件的色階用法跟其他主題的規律不同,有些甚至用了色彩系統以外的新色票。跳色設計,調皮,非常調皮。
https://ithelp.ithome.com.tw/upload/images/20260928/20184345dFvjlfqyUq.png

不哭不哭,眼淚是珍珠,越哭越像豬。明天分享該如何調整現有架構,來滿足設計稿的需求。


上一篇
【Day 13】主題樣式:先利其器,認識 SCSS
下一篇
【Day 15】主題樣式:語意變數與跳色,就你最特別
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言