iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

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

【Day 16】主題樣式:inline SVG 再臨

  • 分享至 

  • xImage
  •  

前兩天,我們用 SCSS 處理了主題樣式,不過仍有部分畫面需要依賴圖片,像是帶有造型的按鈕、裝飾零件等。

今天想來清點這些圖片,看看哪些主題的變化,其實是能交給 CSS 變數。那麼開始吧!


結構一樣,差在顏色

先來看看目前專案的寫法。是把各個主題的圖片一一從設計稿匯出,放到資料夾中。然後在使用到的地方,匯入所有主題的靜態資源、設定對應圖片 map。再依主題名稱查詢 map,切換顯示的圖片。不能說錯,但是對於部分元件,我想或許能改用 inline SVG。

例如帶有 icon 的按鈕,還有 Call to action 按鈕。因為這類主題元件,基本圖形結構都相同,唯一的差別只在顏色變化。再加上這類設計都是向量圖形,剛好能用 inline SVG 處理。

除此之外,還有一點就是 icon、按鈕樣式容易隨著專案發展而擴充,像是播放按鈕、連結按鈕、檔案按鈕、下載按鈕等等。如果有四個主題,那每新增一種按鈕,就需要新增 4 張圖片。這還沒有算到互動樣式 hover、disabled 等,完全能預期靜態檔案數量的成長速度會很可觀。

既然如此,不如利用 Day07 學到的 inline SVG 操作方式,來管理主題 icon 與按鈕!


做成 .vue 元件

做法很單純:把設計稿匯出的 SVG 貼進 .vue 的 template,然後把寫死的色碼換成 var()。顏色就用 Day14 設定好的主題 CSS 顏色變數,以及啟用主題的 .theme-container、切換主題的 .theme-{主題名稱} class。

一般 icon 按鈕

https://ithelp.ithome.com.tw/upload/images/20261001/201843458szFl317Q5.png
以暫停/播放按鈕為例,這是一個菱形按鈕,在需要動態調色的地方(icon 用 fill、邊框用 stroke)寫入 var(--theme-...),再用 props 傳入的 themeName 切換主題色。

<script setup>
defineProps({
  playing: Boolean,
  themeName: { type: String, default: 'coffee' },
})
</script>

<template>
  <svg class="theme-container" :class="`theme-${themeName}`" viewBox="0 0 56 56" aria-hidden="true">
    <rect x="9" y="9" width="38" height="38" rx="10" transform="rotate(45 28 28)" fill="#fff" stroke="var(--theme-primary-500)" stroke-width="3" />

    <!-- v-if 切換圖形:true 時顯示暫停的兩條槓,false 則顯示三角形 -->
    <g v-if="playing" fill="var(--theme-primary-500)">
      <rect x="20.5" y="19" width="5.5" height="18" rx="1.1" />
      <rect x="30" y="19" width="5.5" height="18" rx="1.1" />
    </g>
    <path v-else d="M24 19L37 28L24 37Z" fill="var(--theme-primary-500)" stroke="var(--theme-primary-500)" stroke-width="3" stroke-linejoin="round" />
  </svg>
</template>

💡 選擇把 .theme-container 放入主題元件內,是為了避免父層忘記下 .theme-container 導致樣式出不來。個人覺得使用主題元件時,透過修改 themeName props 的 value 來切換主題,比較直觀。

背景圖飾 + 文字按鈕

https://ithelp.ithome.com.tw/upload/images/20261001/20184345K6IthG1WwL.png
這個範例稍微複雜,按鈕有多畫四顆裝飾的星星、裁切超出外框的星星,還有內文可能改變的文字。文字部分我們用 <text> 搭配 props 接收內文,主題色的部分一樣也是在 fill 寫進主題 CSS 變數:

<script setup>
import { useId } from 'vue'

defineProps({
  text: { type: String, default: '' },
  themeName: { type: String, default: 'coffee' },
})

const STAR_D = 'M12 1.8L15.2 8.2L22.3 9.3L17.1 14.3L18.3 21.3L12 18L5.7 21.3L6.9 14.3L1.7 9.3L8.8 8.2Z'

// 同一頁放多個按鈕時,避免 clipPath 的 id 重複
const clipId = `theme-submit-clip-${String(useId()).replace(/[^a-zA-Z0-9_-]/g, '')}`
</script>

<template>
  <svg class="theme-container shadow" :class="`theme-${themeName}`" viewBox="0 0 185 62">
    <g :clip-path="`url(#${clipId})`">
      <rect width="184" height="61" rx="22" fill="var(--theme-primary-500)" />
      <!-- 亮點星星 -->
      <path :d="STAR_D" fill="var(--theme-primary-300)" transform="..."/>
      <path :d="STAR_D" fill="var(--theme-primary-300)" transform="..."/>
      <!-- 暗色星星 -->
      <path :d="STAR_D" fill="var(--theme-primary-600)" transform="..."/>
      <path :d="STAR_D" fill="var(--theme-primary-600)" transform="..."/>
    </g>
    <rect x="1.5" y="1.5" width="181" height="58" rx="21" fill="none" stroke="#fff" stroke-width="3" />
    <defs>
      <clipPath :id="clipId">
        <rect width="184" height="61" rx="22" />
      </clipPath>
    </defs>
    <text
      v-if="text"
      x="92"
      y="30"
      fill="#fff"
      ...
    >{{ text }}</text>
  </svg>
</template>

💡 如果圖形有用到 <g clip-path="url(#id)">,要注意 clipPath 是用 url(#id) 引用,同一頁引用多次該元件時會因為 id 重複而裁切異常。

這個按鈕原本還有寫在 SVG 的投影 <filter>,視覺上是陰影,但實際會被包含在 SVG 內。如果陰影面積較大,就會把 svg 的範圍撐大。所以要把 Figma 匯出的陰影 <filter> 拿掉,改成對 <svg> 下 CSS 的 filter: drop-shadow():

.shadow {
  filter: drop-shadow(2px 2px 2px rgb(0 0 0 / 0.15));
}

實際使用

使用時,將主題名稱作為 props 傳入,元件就會自己顯示對應的主題色:

<button type="button" class="btn-play" @click="playing = !playing" :aria-label="playing ? '暫停' : '播放'">
  <ThemePlayBtn :playing="playing" :theme-name="themeName" />
</button>
<button type="button" class="btn-submit">
  <ThemeSubmitBtn text="送出答案" :theme-name="themeName" />
</button>

Demo

demo gif

Codepen 連結
Codepen 一個 pen 只提供免費仔 3 個檔案,Vue 的範例都只能寫在同一個 .vue 中,寫法會變的彎彎繞繞,較難直接參考。


後話:補充 SVG sprite

在研究之前專案的寫法時,得知有種寫法叫 SVG sprite。簡單來說就是將所有圖形的設定寫在同一個 <svg> 標籤的 <symbol> 中,再用 <use> 的 href="" 引用不同的圖形。聽起來非常方便。但是這樣不會讓 <svg> 檔案變超長的嗎?

稍微了解了一下,發現實務上多使用套件自動產生 SVG sprite。這邊附上文章,讓大家參考參考:

  • SVG Sprites - Summer。桑莫。夏天
    • 很詳細的講解不同 SVG Icon 的使用方式、優缺點
    • 原來 icon 也能用 base64 製作,已知用火 😮
  • Icon Fonts vs. SVG Sprites vs. Inline SVGs in 2026
    • 2026 年的文章,比較標題中三者的優劣勢
    • React、Next.js 或 Vue 專案,推薦使用 inline SVG。但要注意 DOM Bloat 導致的效能問題,例如在大型表格中用 v-for 或 map() 渲染多個複雜的 SVG。

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

尚未有邦友留言

立即登入留言