聊聊純 CSS 圖標(biāo)
感謝印記中文的 QC-L[1] 對(duì)本文進(jìn)行翻譯,英文原文: English Version[2]。
在 重新構(gòu)想原子化 CSS?中,我提到了 UnoCSS[4] 的一個(gè)預(yù)設(shè),它提供了在純 CSS 中按需使用任何圖標(biāo)的能力。在這篇文章中,我想和大家分享下它的工作原理。
我的圖標(biāo)探索之旅
如果你對(duì)我如何探索圖標(biāo)解決方案的過(guò)程感興趣,下面這個(gè)博文列表,是我在圖標(biāo)探索過(guò)程中總結(jié)和相關(guān)實(shí)驗(yàn)
2020 年 8 月 - 圖標(biāo)探索之旅[5] 2021 年 9 月 - 圖標(biāo)探索之旅后續(xù)[6] 2021 年 10 月 - 重新構(gòu)想原子化 CSS 2021 年 11 月 - 聊聊純 CSS 圖標(biāo) - 你在這里!
現(xiàn)有方案
有個(gè)名為 `css.gg`[8] 的純 CSS 圖標(biāo)解決方案,它完全通過(guò)偽元素(::before,::after)來(lái)構(gòu)建圖標(biāo)。使用這種方案意味著你需要對(duì) CSS 工作原理有深刻的理解,但同時(shí)也很難創(chuàng)造更為復(fù)雜的圖標(biāo)(只有3個(gè)元素可以使用)。我在尋找 一種更加通用化的解決方案,可以適用于任何圖標(biāo) 而并非在特定集合中進(jìn)行有限的選擇。
我的方案
這個(gè)方案來(lái)源于社區(qū)小伙伴 @husayt[9] 在 unplugin-icons 中提出 需求[10] 并由 @userquin[11] 在 此 PR 中[12] 提供了初版的實(shí)現(xiàn)。這個(gè)方案非常簡(jiǎn)單,用 DataURI[13] 中的圖標(biāo)作為背景圖,并生成如下 CSS。
.my-icon?{
??background:?url(data:...)?no-repeat?center;
??background-color:?transparent;
??background-size:?16px?16px;
??height:?16px;
??width:?16px;
??display:?inline-block;
}
有了這種方案,我們就可以使用一個(gè)單獨(dú)的類(lèi)在 CSS 中內(nèi)嵌任何圖像。

這個(gè)想法非常有趣,但是這更其實(shí)更像一張圖片而非圖標(biāo)。對(duì)我而言,一個(gè)圖標(biāo)必須是可以根據(jù)上下文進(jìn)行縮放和著色的。
實(shí)現(xiàn)
DataURI
再次感謝 Iconify[14],它將 100 多個(gè)圖標(biāo)集與上萬(wàn)個(gè)圖標(biāo)統(tǒng)一為 一致的 JSON 格式[15]。它允許我們通過(guò)簡(jiǎn)單地提供集合和圖標(biāo) ID 的方式來(lái)獲取任意圖標(biāo)集中的 SVG,使用方式如下:
import?{?iconToSVG,?getIconData?}?from?'@iconify/utils'
const?svg?=?iconToSVG(getIconData('mdi',?'alarm'))
//?(此處并非真實(shí)?API,僅供示意)
當(dāng)我們得到 SVG 字符串后,可以將其轉(zhuǎn)換為 DataURI:
const?dataUri?=?`data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
說(shuō)到 DataURI,使用 Base64[16] 幾乎一直是我的默認(rèn)選擇 -- 直到我看到 Chris Coyier 所寫(xiě)的 你可能不需要使用 Base64 SVG[17] 文章。對(duì)于圖像等二進(jìn)制數(shù)據(jù)必須使用 Base64 進(jìn)行編碼,以便在 CSS 等純文本文件中使用,而對(duì)于 SVG 來(lái)說(shuō),由于它已經(jīng)是文本格式,所以使用 Base64 編碼實(shí)際上會(huì)使得文件體積變得變大。
結(jié)合 Taylor Hunt 在 優(yōu)化 DataURI 中的 SVG[18] 提到的相關(guān)技術(shù),進(jìn)一步對(duì)輸出大小進(jìn)行了改進(jìn),以下是我們的最終解決方案。
//?https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
function?encodeSvg(svg:?string)?{
??return?svg.replace('可縮放
使 ”圖片“ 更像圖標(biāo)的第一步,我們需要讓它可以根據(jù)上下文進(jìn)行縮放。
幸運(yùn)的是,CSS 為我們提供了原生的縮放支持 —— em 單位。
.my-icon?{
??background:?url(data:...)?no-repeat?center;
??background-color:?transparent;
??background-size:?100%?100%;
??height:?1em;
??width:?1em;
}
通過(guò)改變 height 和 width 為 1em,并設(shè)置 background-size 為 100%,我們可以使得圖片的比例基于其父級(jí)元素的字體大小變化。

可著色
在內(nèi)聯(lián)的 SVG 中,我們可以使用 [fill="currentColor"](https://www.w3.org/TR/css-color-3/#currentcolor "fill="currentColor"") 來(lái)為 SVG 著色。但是,當(dāng)我們將其作為背景圖時(shí),它就變成了一個(gè)圖片。SVG 的動(dòng)態(tài)性消失了,currentColor 的效果也隨之消失(這和你無(wú)法覆蓋 PNG 的顏色一樣)。
如果你 Google 一下,你會(huì)發(fā)現(xiàn)大多數(shù)人都告訴你告訴你,這個(gè)就是個(gè)限制沒(méi)有辦法。少部分人會(huì)給你提供一個(gè)解決方案 -- 在轉(zhuǎn)換為 DataURI 前在 SVG 中設(shè)置顏色,這可以解決對(duì)于特定圖標(biāo)著色的問(wèn)題,但是沒(méi)有從根本上解決上下文著色的問(wèn)題。
此時(shí),可能會(huì)有小伙伴想到使用 CSS filters[19],就像 Una Kravets 在 使用 CSS 給 SVG 背景上色[20] 一文中提到的那樣。聽(tīng)起來(lái)還不錯(cuò),也許引入一些運(yùn)行時(shí)的 JavaScript 去計(jì)算如何將顏色轉(zhuǎn)化為最終所需的顏色矩陣便可以做到。但這就違背了我們?cè)谔剿骷?CSS 中圖標(biāo)的目的。
在我快要放棄這個(gè)方案時(shí),我無(wú)意中發(fā)現(xiàn)了 Noah Blon 的 在 CSS 背景圖片中為 SVGs 上色[21]。文中提到了一個(gè)非常絕妙的主意,通過(guò)使用 CSS masks[22] 對(duì)背景進(jìn)行蒙版 - 一個(gè)從未聽(tīng)說(shuō)過(guò) CSS 屬性。
.my-icon?{
??background-color:?red;
??mask-image:?url(icon.svg);
}
與其想辦法給背景圖片著色,不如換種思路,把圖標(biāo)作為一個(gè)蒙版,來(lái)對(duì)背景的顏色進(jìn)行裁剪。這樣做,我們還可以使用 currentColor 為其著色!
.my-icon?{
??background-color:?currentColor;
??mask-image:?url(icon.svg);
}

彩色圖標(biāo)
我們把單色的圖標(biāo)做成了可著色的,但又遇到了新的問(wèn)題。當(dāng)使用 mask 時(shí),圖標(biāo)的顏色和內(nèi)容會(huì)丟失。例如:

我想,很多時(shí)候可能很難通過(guò)一種方案來(lái)解決所有問(wèn)題。
但是,其實(shí)我們可以使用兩種方案!還記得我們最開(kāi)始提到了將圖像作為背景圖片的方案嗎?這個(gè)不正適用于彩色圖標(biāo) -- 畢竟使用彩色圖標(biāo)時(shí),我們也不需要修改它的顏色。
解決方案其實(shí)很簡(jiǎn)單,我們只需找到一種方法來(lái)巧妙地區(qū)分單色圖標(biāo)和彩色圖標(biāo)。既然我們可以得到 SVG 的內(nèi)容,我們便可以使用如下方法:
//?如果?SVG?的圖標(biāo)包含?`currentColor`?的值
//?它大概率是一個(gè)單色圖標(biāo)
const?mode?=?svg.includes('currentColor')
????'mask'
??:?'background-img'
const?uri?=?`url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`
//?單色圖標(biāo)
if?(mode?===?'mask')?{
??return?{
????'mask':?`${uri}?no-repeat`,
????'mask-size':?'100%?100%',
????'background-color':?'currentColor',
????'height':?'1em',
????'width':?'1em',
??}
}
//?彩色圖標(biāo)
else?{
??return?{
????'background':?`${uri}?no-repeat`,
????'background-size':?'100%?100%',
????'background-color':?'transparent',
????'height':?'1em',
????'width':?'1em',
??}
}
而且最終效果出乎意料的完美!它的效果其實(shí)和我們?nèi)粘=佑|到的一個(gè)工具非常相似 - 系統(tǒng)的原生 Emoji。文本顏色會(huì)根據(jù)上下文而發(fā)生變化,而 Emoji 則保持自己的顏色。
最終效果展示:

如果想要查看或者搜索所有可用的圖標(biāo),可以參考我的另一個(gè)開(kāi)源項(xiàng)目 Ic?nes[23]。
使用
如果你想在項(xiàng)目中嘗試這個(gè)圖標(biāo)解決方案,你可以安裝 UnoCSS[24] 和圖標(biāo)預(yù)設(shè):
npm?i?-D?unocss?@unocss/preset-icons?@iconify/json
@iconify/json 包含了所有 Iconify 收錄的圖標(biāo)集(120MB 左右)?;蛘?,你也可以按圖標(biāo)集的方式進(jìn)行安裝以節(jié)省流量和儲(chǔ)存空間,例如,需使用 Material Design 的圖標(biāo),你可以安裝 @iconify-json/mdi,使用 Carbon 的圖標(biāo),你可以安裝 @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`?被指定時(shí),默認(rèn)的預(yù)設(shè)將會(huì)被禁用,
??????//?因此你可以在你原有的 App 上使用純 CSS 圖標(biāo)而不需要擔(dān)心 CSS 沖突的問(wèn)題。
??????presets:?[
????????UnocssIcons({
??????????//?其他選項(xiàng)
??????????prefix:?'i-',
??????????extraProperties:?{
????????????display:?'inline-block'
??????????}
????????}),
????????//?presetUno()?-?取消注釋以啟用默認(rèn)的預(yù)設(shè)
??????],
????}),
??],
})
這就是今天的全部?jī)?nèi)容了。希望你能喜歡這個(gè)來(lái)自 UnoCSS 的圖標(biāo)解決方案,或者能為你提供靈感,用于你自己的項(xiàng)目。
感謝閱讀,下次見(jiàn) :)
參考資料
QC-L: https://github.com/QC-L
[2]English Version: https://antfu.me/posts/icons-in-pure-css
[3]重新構(gòu)想原子化 CSS: https://antfu.me/posts/reimagine-atomic-css#pure-css-icons
[4]UnoCSS: https://github.com/antfu/unocss
[5]圖標(biāo)探索之旅: https://antfu.me/posts/journey-with-icons
[6]圖標(biāo)探索之旅后續(xù): https://antfu.me/posts/journey-with-icons-continues
[7]重新構(gòu)想原子化 CSS: https://antfu.me/posts/reimagine-atomic-css-zh
[8]css.gg: https://github.com/astrit/css.gg
@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]優(yōu)化 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

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專(zhuān)業(yè)的技術(shù)人...


