有個名為 `css.gg`[8] 的純 CSS 圖標解決方案,它完全通過偽元素(::before,::after)來構建圖標。使用這種方案意味著你需要對 CSS 工作原理有深刻的理解,但同時也很難創造更為複雜的圖標(只有3個元素可以使用)。我在尋找 一種更加通用化的解決方案,可以適用於任何圖標 而並非在特定集合中進行有限的選擇。
我的方案這個方案來源於社區小夥伴 @husayt[9] 在 unplugin-icons 中提出 需求[10] 並由 @userquin[11] 在 此 PR 中[12] 提供了初版的實現。這個方案非常簡單,用 DataURI[13] 中的圖標作為背景圖,並生成如下 CSS。
.my-icon {
background: url(data:...) no-repeat center;
background-color: transparent;
background-size: 16px 16px;
height: 16px;
width: 16px;
display: inline-block;
}有了這種方案,我們就可以使用一個單獨的類在 CSS 中內嵌任何圖像。
這個想法非常有趣,但是這更其實更像一張圖片而非圖標。對我而言,一個圖標必須是可以根據上下文進行縮放和著色的。
實現DataURI再次感謝 Iconify[14],它將 100 多個圖標集與上萬個圖標統一為 一致的 JSON 格式[15]。它允許我們通過簡單地提供集合和圖標 ID 的方式來獲取任意圖標集中的 SVG,使用方式如下:
import { iconToSVG, getIconData } from '@iconify/utils'
const svg = iconToSVG(getIconData('mdi', 'alarm'))
// (此處並非真實 API,僅供示意)當我們得到 SVG 字符串後,可以將其轉換為 DataURI:
const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`說到 DataURI,使用 Base64[16] 幾乎一直是我的默認選擇 -- 直到我看到 Chris Coyier 所寫的 你可能不需要使用 Base64 SVG[17] 文章。對於圖像等二進位數據必須使用 Base64 進行編碼,以便在 CSS 等純文本文件中使用,而對於 SVG 來說,由於它已經是文本格式,所以使用 Base64 編碼實際上會使得文件體積變得變大。
結合 Taylor Hunt 在 優化 DataURI 中的 SVG[18] 提到的相關技術,進一步對輸出大小進行了改進,以下是我們的最終解決方案。
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
function encodeSvg(svg: string) {
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
.replace(/"/g, '\'')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
}
const dataUri = `data:image/svg+xml;utf8,${encodeSvg(svg)}`
可縮放使 」圖片「 更像圖標的第一步,我們需要讓它可以根據上下文進行縮放。
幸運的是,CSS 為我們提供了原生的縮放支持 —— em 單位。
.my-icon {
background: url(data:...) no-repeat center;
background-color: transparent;
background-size: 100% 100%;
height: 1em;
width: 1em;
}通過改變 height 和 width 為 1em,並設置 background-size 為 100%,我們可以使得圖片的比例基於其父級元素的字體大小變化。
可著色
在內聯的 SVG 中,我們可以使用 [fill="currentColor"](https://www.w3.org/TR/css-color-3/#currentcolor "fill="currentColor"") 來為 SVG 著色。但是,當我們將其作為背景圖時,它就變成了一個圖片。SVG 的動態性消失了,currentColor 的效果也隨之消失(這和你無法覆蓋 PNG 的顏色一樣)。
如果你 Google 一下,你會發現大多數人都告訴你告訴你,這個就是個限制沒有辦法。少部分人會給你提供一個解決方案 -- 在轉換為 DataURI 前在 SVG 中設置顏色,這可以解決對於特定圖標著色的問題,但是沒有從根本上解決上下文著色的問題。
此時,可能會有小夥伴想到使用 CSS filters[19],就像 Una Kravets 在 使用 CSS 給 SVG 背景上色[20] 一文中提到的那樣。聽起來還不錯,也許引入一些運行時的 JavaScript 去計算如何將顏色轉化為最終所需的顏色矩陣便可以做到。但這就違背了我們在探索純 CSS 中圖標的目的。
在我快要放棄這個方案時,我無意中發現了 Noah Blon 的 在 CSS 背景圖片中為 SVGs 上色[21]。文中提到了一個非常絕妙的主意,通過使用 CSS masks[22] 對背景進行蒙版 - 一個從未聽說過 CSS 屬性。
.my-icon {
background-color: red;
mask-image: url(icon.svg);
}與其想辦法給背景圖片著色,不如換種思路,把圖標作為一個蒙版,來對背景的顏色進行裁剪。這樣做,我們還可以使用 currentColor 為其著色!
.my-icon {
background-color: currentColor;
mask-image: url(icon.svg);
}彩色圖標
我們把單色的圖標做成了可著色的,但又遇到了新的問題。當使用 mask 時,圖標的顏色和內容會丟失。例如:
我想,很多時候可能很難通過一種方案來解決所有問題。
但是,其實我們可以使用兩種方案!還記得我們最開始提到了將圖像作為背景圖片的方案嗎?這個不正適用於彩色圖標 -- 畢竟使用彩色圖標時,我們也不需要修改它的顏色。
解決方案其實很簡單,我們只需找到一種方法來巧妙地區分單色圖標和彩色圖標。既然我們可以得到 SVG 的內容,我們便可以使用如下方法:
// 如果 SVG 的圖標包含 `currentColor` 的值
// 它大概率是一個單色圖標
const mode = svg.includes('currentColor')
? 'mask'
: 'background-img'
const uri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`
// 單色圖標
if (mode === 'mask') {
return {
'mask': `${uri} no-repeat`,
'mask-size': '100% 100%',
'background-color': 'currentColor',
'height': '1em',
'width': '1em',
}
}
// 彩色圖標
else {
return {
'background': `${uri} no-repeat`,
'background-size': '100% 100%',
'background-color': 'transparent',
'height': '1em',
'width': '1em',
}
}而且最終效果出乎意料的完美!它的效果其實和我們日常接觸到的一個工具非常相似 - 系統的原生 Emoji。文本顏色會根據上下文而發生變化,而 Emoji 則保持自己的顏色。
最終效果展示:
如果想要查看或者搜索所有可用的圖標,可以參考我的另一個開源項目 Icônes[23]。
使用如果你想在項目中嘗試這個圖標解決方案,你可以安裝 UnoCSS[24] 和圖標預設:
npm i -D unocss @unocss/preset-icons @iconify/json@iconify/json 包含了所有 Iconify 收錄的圖標集(120MB 左右)。或者,你也可以按圖標集的方式進行安裝以節省流量和儲存空間,例如,需使用 Material Design 的圖標,你可以安裝 @iconify-json/mdi,使用 Carbon 的圖標,你可以安裝 @iconify-json/carbon 等。
接著配置你的 vite.config.js:
import { defineConfig } from 'vite'
import Unocss from 'unocss'
import UnocssIcons from '@unocss/preset-icons'
export default defineConfig({
plugins: [
Unocss({
// 但 `presets` 被指定時,默認的預設將會被禁用,
// 因此你可以在你原有的 App 上使用純 CSS 圖標而不需要擔心 CSS 衝突的問題。
presets: [
UnocssIcons({
// 其他選項
prefix: 'i-',
extraProperties: {
display: 'inline-block'
}
}),
// presetUno() - 取消注釋以啟用默認的預設
],
}),
],
})這就是今天的全部內容了。希望你能喜歡這個來自 UnoCSS 的圖標解決方案,或者能為你提供靈感,用於你自己的項目。
感謝閱讀,下次見 :)
參考資料[1]QC-L: https://github.com/QC-L
[2]English Version: https://antfu.me/posts/icons-in-pure-css
[3]重新構想原子化 CSS: https://antfu.me/posts/reimagine-atomic-css#pure-css-icons
[4]UnoCSS: https://github.com/antfu/unocss
[5]圖標探索之旅: https://antfu.me/posts/journey-with-icons
[6]圖標探索之旅後續: https://antfu.me/posts/journey-with-icons-continues
[7]重新構想原子化 CSS: https://antfu.me/posts/reimagine-atomic-css-zh
[8]css.gg: https://github.com/astrit/css.gg
[9]@husayt: https://github.com/husayt
[10]需求: https://github.com/antfu/unplugin-icons/issues/88
[11]@userquin: https://github.com/userquin
[12]此 PR 中: https://github.com/antfu/unplugin-icons/pull/90
[13]DataURI: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[14]Iconify: https://iconify.design/
[15]一致的 JSON 格式: https://github.com/iconify/collections-json
[16]Base64: https://developer.mozilla.org/en-US/docs/Glossary/Base64
[17]你可能不需要使用 Base64 SVG: https://css-tricks.com/probably-dont-base64-svg/
[18]優化 DataURI 中的 SVG: https://codepen.io/Tigt/post/optimizing-svgs-in-data-uris
[19]CSS filters: https://developer.mozilla.org/en-US/docs/Web/CSS/filter
[20]使用 CSS 給 SVG 背景上色: https://css-tricks.com/solved-with-css-colorizing-svg-backgrounds/
[21]在 CSS 背景圖片中為 SVGs 上色: https://codepen.io/noahblon/post/coloring-svgs-in-css-background-images
[22]CSS masks: https://developer.mozilla.org/en-US/docs/Web/CSS/mask
[23]Icônes: https://icones.js.org/
[24]UnoCSS: https://github.com/antfu/unocss