iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

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

【Day 15】主題樣式:語意變數與跳色,就你最特別

  • 分享至 

  • xImage
  •  

昨天用 map 與 @each 組合出各個主題的 CSS 顏色變數,再用這些變數來寫主題樣式。正當我們高高興興地要回城,卻被設計稿的更動 Gank 了:部分主題樣式的用色規律跟其他主題不同,有些甚至用了新顏色。

當然,可以把特例直接寫在 .vue、.jsx 元件內。但主題樣式在專案中很多地方都會用到,一個個設定的話,特例會散落在各個元件裡,顏色也不再集中在 map。

今天,我們接著擴寫主題檔,讓有特例的元件不再讀「什麼顏色」,而是讀「用在哪裡」。那麼開始吧!


一、語意變數:用途當 key

處理跳色問題,我的想法是:在主題 map 裡,為跳色的元件增加以「用途」為名的 key。這樣從名稱就能看出哪些元件的用色不同,也能同時處理兩種跳色:

  1. 不同主題用了不同的色階
  2. 用了色階外的顏色

以選項按鈕 btn-option-bg、卡片背景 card-bg 為例:

$themes: (
  coffee: (
    primary: (100: #ffe9d2, 500: #ff7300),
    accent:  (100: #fff1db),
    btn-option-bg: #ff7300,
    card-bg: #fff1db,
  ),
  matcha: (
    primary: (100: #ddfee7, 500: #10c659),
    accent:  (100: #ecffcf),
    btn-option-bg: #10c659,
    card-bg: #fcefbd,   // 跳色:色階外的顏色
  ),
);

// 要輸出成 --theme-<key> 的語意色票
$var-keys: (btn-option-bg, card-bg);

$theme-default 也要補上這兩個 key。接著在 theme-vars 的最後,把語意色票輸出成變數:

@mixin theme-vars($name, $theme) {
  // ...Day 14 的必填色階檢查

  @each $key in $var-keys {
    --theme-#{$key}: #{map.get($theme, $key)};
  }
}

編譯後(節錄 matcha):

.theme-container.theme-matcha {
  /* ...色階變數 */
  --theme-btn-option-bg: #10c659;
  --theme-card-bg: #fcefbd;
}

跳色元件改讀語意變數:

.theme-container {
  .theme-btn-option { background-color: var(--theme-btn-option-bg); }
  .theme-card { background-color: var(--theme-card-bg); }
}

二、參照色階

語意色票解決了跳色,但產生了新問題:語意色票得再手寫一次色階的色碼,例如 btn-option-bg: #10c659 其實就是 matcha 的 primary 500。同一個顏色有兩個來源,之後要改色時,還得記得兩邊一起改,嘖嘖。

同樣的事不幹第二次,所以要讓語意色票也能接受「色系 色階」的寫法,讓它參照自己主題的色階:

matcha: (
  primary: (100: #ddfee7, 500: #10c659),
	// ...
  btn-option-bg: primary 500,
  // ...
),

💡 primary 500 中間用空格隔開,在 Sass 裡會被視為一個 list。

接下來將 $theme 傳入自訂的轉換函式 resolve-theme(),一一檢查每個 value,如果遇到 list 就換成對應的實際色碼,其他值則直接寫入:

@use "sass:list";
@use "sass:meta";

@function resolve-theme($name, $theme) {
  $resolved: ();   // 用來儲存解析結果的 map
  
  @each $key, $value in $theme {
    @if meta.type-of($value) == list {
	  // 從該主題的 map 中取得對應的色碼
      $value: map.get($theme, list.nth($value, 1), list.nth($value, 2));
    }
    $resolved: map.set($resolved, $key, $value);
  }
  
  @return $resolved;
}

💡 SCSS 內建函式回傳了什麼?

  • meta.type-of() 回傳類型:#ff7300 會回傳 color;primary 500 則會回傳 list
  • list.nth(list, 順序) 回傳指定順序的值:list.nth(primary 500, 1) 會回傳 primary;list.nth(primary 500, 2) 則會回傳 500

回到 theme-vars 裡,檢查完必填色階後,先把主題解析一次再輸出:

@mixin theme-vars($name, $theme) {
  // ...Day 14 的必填色階檢查
  
	$theme: resolve-theme($name, $theme);
	
  @each $key in $var-keys {
    // ...
  }
}

三、檢查必填色票與參照

Day 14 已經檢查了必填色階,現在多了語意色票和參照,也一起加入檢查:

@mixin theme-vars($name, $theme) {
  // ...Day 14 的必填色階檢查

  // 必填色票
  @each $key in $var-keys {
    @if not map.has-key($theme, $key) {
      @error "主題 #{$name} 缺少色票 #{$key}";
    }
  }
  
  $theme: resolve-theme($name, $theme);
  // ...輸出
}

參照是否存在,則是在 resolve-theme() 裡、換成色碼之前檢查:

@if meta.type-of($value) == list {
  @if not map.has-key($theme, list.nth($value, 1), list.nth($value, 2)) {
    @error "主題#{$name} 的 #{$key} 參照了不存在的色階:#{$value}";
  }
  $value: map.get($theme, list.nth($value, 1), list.nth($value, 2));
}

實際觸發的錯誤訊息:

情境 錯誤訊息
coffee 沒寫 btn-option-bg 主題 coffee 缺少色票 btn-option-bg
matcha 寫成 card-bg: accent 5000 主題 matcha 的 card-bg 參照了不存在的色階:accent 5000

完整範例

Codepen範例(原生 + SCSS)

.

.

.

在這個架構下:

  1. 已完成設定的任何更動,只需要關注 $themes 色票設定。
  2. 新增主題,若有漏填的色票會在編譯階段就出現錯誤提醒。
  3. 新增跳色元件或色階,分別在色票 map 與檢查 map 追加 key 即可。

.theme- 開頭的主題樣式只要寫一次,之後基本上不用再改。一天又平安地過去了,感謝 SCSS 的努力!

純 CSS 能解決的主題樣式,介紹到這邊差不多告一段落。明天,來看看那些不得不用圖片處理的樣式吧!


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

尚未有邦友留言

立即登入留言