使用及管理圖示

選擇圖示技術

圖示儲存和算繪是不同的選項。舉例來說,SVG Sprite 可以為 CSS 圖示系統提供圖示,或使用 <use> 參照。

  • CSS 圖示系統:如果 CSS 應選擇顯示圖示的時間和圖示,請使用這個系統。單色圖示請使用遮罩,全彩圖示則使用背景圖片。
  • 使用 <use> 的 SVG:如果內建樣式選項已足夠,請使用這個選項重複使用符號。CSS 無法任意設定所參照 SVG 內容的樣式或動畫,但如果符號設計為使用繼承屬性和自訂屬性,則可傳入這些屬性。舉例來說,路徑可以使用 fill="var(--icon-accent, currentColor)",讓 CSS 為圖示的該部分上色。
  • 內嵌 SVG:如需任意設定 SVG 內容的樣式或動畫,請使用這個選項。
  • <img> 搭配 CSS 濾鏡:請避免使用這種方式重新著色圖示。篩選器無法可靠地將任意 CSS 顏色值套用至圖片,因此重新著色相當繁瑣且不靈活。

請思考以下兩個問題,CSS 是否需要挑選或更換圖示 (元件預設值、狀態變更、主題設定)?如果是,請使用 CSS 圖示系統。除了顏色之外,您是否需要調整圖示內部?如果是,且您能控制圖示來源,請使用自訂屬性設計符號,並使用 <use>;如果無法控制來源,請內嵌 SVG。第三方圖示集很少會公開自訂屬性,因此對其使用 <use> 只會提供顏色,其他內容則很少。

內嵌或遠端圖示

CSS 圖示系統和 <use> 都可以從資料 URI、個別檔案或 Sprite 擷取圖示。嵌入 (資料 URI 或內嵌 SVG) 可避免要求,並在首次顯示時算繪,但會增加 CSS 或 HTML,且無法單獨快取,因此適合用於在每個頁面上顯示的一小組圖示。個別檔案會獨立快取,並視需要載入,每次載入會產生一個要求。Sprite 是針對整組圖示的可快取要求,但無論是否使用,都會下載每個圖示,且 <use> 只能參照來自相同來源的 Sprite。建議嵌入少數核心圖示和一個精靈圖,或為較大的組合嵌入個別檔案。

如果網頁一次轉譯數千個圖示,請對實際網頁和素材資源進行基準測試。在壓力測試中,內嵌 SVG 在該規模下具有些微效能優勢,但結果取決於瀏覽器和實作方式。Cloud Four 的 SVG 圖示壓力測試

主要方法:CSS 驅動的圖示引擎

圖示是設計符記:每個圖示都是含有圖片的自訂屬性,任何元素只要設定 --icon-start 或 --icon-end,就能顯示圖示。這個引擎結合了三項 CSS 功能:

  • 已註冊的自訂屬性 (@property) 會防止 --icon-start 和 --icon-end 沿用,因此在元件上設定的圖示不會串聯至其子項。
  • 容器樣式查詢 (@container style(...)) 會偵測元素是否已指派圖示並算繪,不需變更類別或標記。
  • CSS 遮罩和 currentColor 會在虛擬元素中算繪圖示,並沿用元素的文字顏色。大小、顏色、間距和垂直偏移量會從可繼承的 --icon-style-* 屬性讀取,並提供合理的備用值,因此整個子樹狀結構可以從祖系重新設定樣式。--icon-style- 前置字元可避免這些設定與圖示權杖本身衝突,圖示權杖也位於 --icon-* 下方。

定義圖示設計符記

使用內嵌 SVG 資料 URI 或根相對網址 (例如 url("/icons/trash.svg")),將 SVG 圖示和任何 --icon-style-* 覆寫定義為可重複使用的自訂屬性。從外部 SVG 檔案載入圖示時,請使用根相對網址或嵌入的資料 URI。透過自訂屬性使用的相對網址,可以解析為使用圖示的樣式表,而非定義自訂屬性的樣式表。如果這些樣式表位於不同目錄,網址可能會解析為錯誤的檔案。

如要讓網址與定義圖示符記的樣式表保持相對關係,可以使用已註冊的屬性:已註冊的 <url> 或 <image> 的計算值會根據宣告該屬性的樣式表進行解析。請勿依賴將權杖註冊為 <image>,並在 initial-value 中使用網址。Chrome 和 Safari 會根據文件解析 initial-value 網址,因此只有 Firefox 支援。除非語法開頭為 <url>,否則 Chrome 也會根據文件解析 :root 上宣告的值。

如果無法避免使用相對網址 (例如不同目錄中的樣式表),請使用開頭為 <url> 的語法註冊權杖,並在 :root 宣告實際網址。這會在每個瀏覽器中解析宣告樣式表:

@property --icon-caret {
  /* The syntax must start with <url>. A syntax that starts with <image> resolves against the document in Chrome. */
  syntax: "<url> | <image> | none";
  inherits: true;
  initial-value: none;
}

:root {
  /* Declared here, the URL resolves against this stylesheet */
  --icon-caret: url("icons/caret.svg");
}

現在每個圖示都位於兩個位置,且有更多樣板,因此只有在專案有不同目錄中的樣式表和可管理的圖示數量時,才值得這麼做。如果圖示超過十幾個,請使用 CSS.registerProperty 透過指令碼註冊 --icon-* 符記 (不含 --icon-style-*),並使用相同的語法,而不是手動編寫每個 @property。註冊的屬性是全域屬性,不適用於 Shadow Root,因此可能不適用於所有專案。如果可以選擇根相對網址或資料 URI,這些網址或 URI 會更簡單,且適用於任何地方。

:root {
  /* Optional: override the built-in fallbacks (1em, currentColor, 0.3em, -0.02em) */
  --icon-style-size: 1em;
  --icon-style-spacing: 0.4em;
  --icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6h16Z'/%3E%3C/svg%3E");
  --icon-favorite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}

從權杖轉譯圖示

元素會將 --icon-start (前置) 或 --icon-end (後置) 設為上述其中一個符記,藉此顯示圖示。兩段 CSS 會將該指派轉換為算繪的圖示。

首先,請向 @property 註冊 --icon-start 和 --icon-end。"*" 語法可接受任何值,而 inherits: false 是重要部分:如果沒有這個屬性,按鈕上設定的圖示會繼承到每個子系,且每個子系也會算繪圖示。

@property --icon-start {
  syntax: "*";
  inherits: false;
}

@property --icon-end {
  syntax: "*";
  inherits: false;
}

第二,容器樣式查詢會偵測元素是否設定了屬性,並在虛擬元素中算繪圖示。由於虛擬元素不會繼承 --icon-start (已註冊為非繼承),因此第一個宣告會使用 inherit 明確轉送值。其餘宣告則處理大小、遮罩、對齊和間距。由於填滿預設為 currentColor,圖示會採用元素的文字顏色,並遵循其轉換。

/* Style query for elements configuring a start icon */
@container style(--icon-start) {
  ::before {
    --icon-start: inherit; /* Forward the value inside the query block */
    content: "" / var(--icon-style-alt, "");
    display: inline-block;
    width: var(--icon-style-size, 1em);
    height: var(--icon-style-size, 1em);
    background-color: var(--icon-style-color, currentColor);
    mask: var(--icon-start) no-repeat center / contain;
    /* Center on the cap height, nudged slightly up for optical balance */
    vertical-align: calc(0.5cap - var(--icon-style-size, 1em) / 2 + var(--icon-style-voffset, -0.02em));
    flex: none;
    pointer-events: none;
    forced-color-adjust: none;
  }

  /* Automatically add spacing ONLY if the element has other text or sibling content */
  :where(:not(.icon, :empty))::before {
    margin-inline-end: var(--icon-style-spacing, 0.3em);
  }
}

每個宣告都有其作用:

  • vertical-align:middle 會對齊 x 高度的一半,在大多數圖示集中,這個位置明顯偏低。以 0.5cap 為中心,可讓圖示中心在 --icon-style-size 增加時保持固定,而微小的負 --icon-style-voffset 則可補償眼睛對圖示上方空間的重視程度高於下方空間。
  • flex: none:空白虛擬元素的最小大小為零,因此在彈性按鈕中,當標籤過長時,它會最先縮小,甚至可能完全消失。圖示大小是設計決策,不應由版面配置壓力協商。
  • pointer-events: none:防止圖示攔截點擊或成為 event.target。
  • forced-color-adjust: none:在強制顏色模式下,瀏覽器會取代 background-color,導致遮罩圖示消失。遮罩填滿會保留,currentColor 仍會遵循強制文字顏色。
  • content: "" / var(--icon-style-alt, ""):生成內容的替代文字可讓僅含圖示的控制項透過 --icon-style-alt 從圖示取得無障礙元素名稱 (請參閱下方的「僅含圖示的控制項」)。預設的空白替代文字可讓輔助技術隱藏裝飾圖示。

如果是結尾圖示,請按照相同模式,使用 --icon-end 和 ::after,而非 --icon-start 和 ::before。使用 mask: var(--icon-end),並以 margin-inline-start 套用間距,而非 margin-inline-end。

獨立和僅含圖示的元素

如果是獨立圖示或僅限圖示的按鈕,請直接將 .icon 套用至空白元素 (僅限圖示控制項的按鈕本身),並使用 --icon 設定其圖示。在這裡,--icon 是較高層級的屬性;.icon:empty 規則會將其對應至 --icon-start,用於前置圖示:

.icon:empty {
  --icon-start: var(--icon);
}

用法範例

樣式表中的元件預設值

將圖示設為自訂屬性的主要好處,在於圖示會參與階層。在選取器上設定圖示,每個相符元素都會套用該圖示,不需變更標記:

.button-delete {
  --icon-start: var(--icon-trash);
}

a[href^="http"]:not([href*="example.com"]) {
  --icon-end: var(--icon-external);
}
<button class="button-delete">Delete item</button>

根據狀態變更圖示

由於圖示只是屬性,因此狀態變更是一般的 CSS 覆寫。沒有要顯示或隱藏的第二個元素,也沒有要切換的類別:

.button-expand {
  --icon-start: var(--icon-caret-right);
}

.button-expand[aria-expanded="true"] {
  --icon-start: var(--icon-caret-down);
}

呈現方式也一樣:在工具列等容器上設定 --icon-style-size、--icon-style-color 或 --icon-style-spacing,其中的每個圖示都會更新。

使用內嵌樣式的單次圖示

如果元素未涵蓋在任何元件規則中,則可設定相同的內嵌屬性,例如 <button style="--icon-start: var(--icon-trash)">。請將此視為應急措施,而非預設做法:如果同一個圖示在同一種元素上出現多次,請將其移至樣式表規則中。

僅限圖示控制項

圖示專用按鈕會使用上述的 .icon 類別和 --icon。使用 aria-label 為按鈕提供無障礙元素名稱,或透過 --icon-style-alt 讓圖示提供名稱;如果兩者都沒有,CSS 產生的圖示就是裝飾性圖示。如果是獨立的資訊圖示,請使用 role="img" 和 aria-label 公開 .icon 元素:

.button-close {
  --icon: var(--icon-close);
  --icon-style-alt: "Close";
}
<!-- Icon-only interactive control, icon and name from the stylesheet -->
<button class="icon button-close"></button>

<!-- Icon-only interactive control, named via aria-label -->
<button class="icon" style="--icon: var(--icon-trash);" aria-label="Delete item"></button>

<!-- Standalone informative icon -->
<span class="icon" style="--icon: var(--icon-favorite);" role="img" aria-label="Favorite"></span>

直接使用 SVG

如果 CSS 圖示系統不適用,請同時使用內嵌 SVG 和 <use>,而不是擇一使用。在圖示首次出現在網頁上時內嵌 SVG,並在每次重複時使用 <use href="#id"> 參照;本機參照不會產生要求,也沒有跨源限制。為多個頁面共用的圖示保留外部 Sprite (<use href="/icons.svg#id">)。

使用 <use> 時,參照的內容會複製到 Shadow Tree,因此網頁 CSS 無法選取其元素。繼承的屬性則會套用:color、currentColor 和任何自訂屬性。除了 SVG 呈現屬性外,現代 CSS 還能變更許多幾何屬性,因此你控制的圖示可以準確顯示所需的掛鉤,例如特定路徑上的 stroke-width="var(--icon-stroke, 2)" 甚至 d: var(--icon-path, path(…))。

Safari 不支援 CSS d,因此只有在可接受這項支援落差時,才使用這項自訂功能。如果無法控制圖示,請將其內嵌。

<use> 個值得留意的問題:

  • viewBox 必須位於 <symbol> 或參照的 <svg> 上;包裝 <use> 的外部 <svg> 不會從參照中擷取,因此請在包裝函式上設定 width 和 height,並保留 viewBox 和符號。
  • 樣式不會跨越任何方向的邊界:外部精靈圖內的 <style> 不會套用至頁面,頁面樣式表也不會套用至參照樹狀結構內。只有繼承屬性和自訂屬性會傳遞。
  • 外部參照僅限同源,跨源精靈圖會失敗,且不會顯示任何訊息。
  • 內嵌至文件的精靈會共用頁面的 id 命名空間,因此請使用命名空間符號 ID,避免發生衝突。

使用 aria-hidden="true" 標記裝飾性 SVG,並為包含的控制項指定專屬無障礙元素名稱,就像 CSS 圖示系統一樣。

備用策略

已註冊的自訂屬性

如果沒有 @property,--icon-start 和 --icon-end 的行為會與一般繼承自訂屬性相同,因此在元素上設定的圖示也會在子系上算繪。如要避免這種情況,請在樣式查詢 (@container style(--icon-start) { * { --icon-start: initial; } }) 內重設子項的屬性,這與下方 Safari 解決方法採用的做法相同。

容器樣式查詢

如要支援不支援容器樣式查詢的瀏覽器,請使用類別或元件選取器做為備用掛鉤。在自訂屬性中保留圖示選取項目;如果只使用內嵌樣式選取器,會遺漏透過 CSS 規則指派的圖示。

:where(.icon-start, .icon:empty)::before {
  content: image-set(linear-gradient(transparent 0 100%), var(--icon-start)) / var(--icon-style-alt, "");
  /* icon styles */
}

:where(.icon-start)::before {
  --icon-start: inherit;
}

:where(.icon:empty)::before {
  --icon-start: var(--icon);
}

:where(.icon-start:not(.icon, :empty))::before {
  margin-inline-end: var(--icon-style-spacing, 0.3em);
}

如要使用結尾圖示,請使用相同的類別掛鉤備用方案,並搭配 .icon-end、--icon-end 和 ::after,而非 .icon-start、--icon-start 和 ::before。使用 margin-inline-start 而非 margin-inline-end,為非空白內容套用間距。

這個 image-set() 模式會在未設定自訂屬性時,使 content 無效,因此會改用 none,且虛擬元素會消失,而不會新增或移除類別。這項操作不會移除虛擬元素的其他樣式,也不會避免與其他 ::before 或 ::after 規則發生衝突,因此請只在這些虛擬元素可用於圖示時,才使用這些掛鉤。

Safari 瀏覽器解決方法

Safari 有兩個會影響圖示的算繪錯誤:WebKit #301609 可能會導致圖示在使用者互動前無法顯示,而 #320220 也會影響圖示的算繪。以下解決方法可同時解決這兩個問題:

@keyframes webkit-301609 {}

@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
  @layer webkit-301609-fix {
    ::before, ::after {
      animation: webkit-301609 0s;
    }
  }
}

@supports 查詢僅指定 Safari,@layer 則可減少動畫衝突。

WebKit 錯誤 #325900 可能會導致直接覆寫 --icon-start 在某些情況下無法生效,例如在懸停時變更圖示。這會影響非繼承註冊屬性的解決方法。如果是 Safari,請將 --icon-start 註冊為繼承,並在樣式查詢中的子系上重設:

@supports ((-webkit-nbsp-mode: normal) or
           (-webkit-touch-callout: none) or
           (font: -apple-system-body)) and (content-visibility: auto) {
  /* Override previous declaration */
  @property --icon-start {
    syntax: "*";
    inherits: true;
  }

  @layer webkit-325900-fix {
    @container style(--icon-start) {
      * {
        --icon-start: initial;
      }
    }
  }
}

重設後,圖示值就不會傳播至子系。將其保留在圖層中,可讓未分層的圖示宣告優先於重設。