聊聊純 CSS 圖標

2021-12-24 前端巔峰
現有方案

有個名為 `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

相關焦點

  • css大法之使用偽元素—實現超實用的圖標庫
    偽元素的知識以及如何用css偽元素來減輕javascript的壓力,做出一些腦洞大開的圖形。偽類用來表示無法在CSS中輕鬆或者可靠檢測到的某個元素的狀態或屬性,比如a標籤的hover表示滑鼠經過的樣式,visited表示訪問過的連結的樣式,更多的用來描述元素狀態變化時的樣式,偽類主要有:我們利用css偽類和偽元素可以實現很多強大的視覺效果,這裡我主要介紹偽元素,如果對偽類或其他css特性感興趣,可以看看我之前的css文章,
  • 《前端5分鐘》之使用純css實現網站換膚和焦點圖切換動畫
    你將收穫網站換膚設計方案介紹:target偽類介紹和用法以及如何使用css實現網站換膚transition動畫以及如何用純css實現焦點圖動畫效果展示1.網站換膚2.焦點圖動畫最簡單的輪播圖組件fancyBox 可以為頁面上的圖片、html 內容和多媒體添加縮放功能sly 導航式、可單向滾動Sequence 可以創建響應式幻燈片、演示、旗幟廣告和以步驟為基礎的CSS 動畫框架PhotoSwipe 適用於行動裝置和桌面電腦,基於原生JavaScript的模塊組件以上介紹的方案都很成熟,我們可以直接拿來使用,但是為了追求簡潔和代碼量最低,我們有辦法用純css
  • 用 CSS3 畫心形和搜索放大鏡圖標
    把上面的兩個圓移到div適當的位置,心形就出來了.heart:before {     left: -50%;     top: 0; } .heart:after {     right: 0;     top: -50%; } 5、最後效果,可以給它加個 css3
  • 純CSS實現簡單骨骼動畫
    骨骼動畫▼關於前端骨骼動畫的實現可以參考《骨骼動畫原理與前端實現淺談》,裡面簡單提及了css和canvas兩種實現方式。這裡將以這個心願牌擺動動畫為例,和大家一起研究如何使用css來實現。那麼js實現中是通過先計算大腿位置,再由大腿位置計算小腿位置來實現聯結的,而css該怎麼實現呢?2.5 純css實現回顧最關鍵的地方:關鍵元素帶著子元素一起運動,子元素在此基礎上自己運動。,要實現關鍵元素和子元素一起運動,在css裡面,只要關鍵元素包裹子元素即可!,這就是css實現骨骼動畫的基石。
  • 聊聊Instagram的新圖標
    但是今天發現朋友圈被Instagram的新圖標刷屏了,想到自從做產品策劃以來,很久沒聊過設計了,今天順著這個話題聊聊久違的設計。上圖:在Instagram的Blog上,用戶對於新圖標的評論,很多人表示不喜歡。首先,我們看看Instagram新舊圖標的對比:
  • 好玩的 CSS - 40 個有趣的 CSS 網站
    htmlcheatsheet.com/css/[4] 交互式在線 CSS 速記表zh.learnlayout.com/[5] 學習 CSS 布局css-tricks.com/[6] CSS 小花招web.dev/learn/css/[7] web.dev 上的 CSS 教學專欄www.w3schools.com/howto/[8] W3 How To 學習如何實現常見的頁面組件codemyui.com/tag/pure-cs…[9] 450 多個純 CSS
  • CSS改變SVG顏色
    項目中我們會用到很多SVG圖標,有時候需要切換某些圖標的顏色。我們可以把圖標複製一份,修改SVG文件裡面的顏色來達到效果。
  • Ionic使用Iconfont-阿里巴巴矢量圖標庫
    Ionic有個自己的圖標庫,但是有些需要的圖標還是沒有,下面介紹一下阿里巴巴的矢量圖標庫,在Ionic中如何使用由於度娘限制,自己百度下載地址吧~輸入「阿里巴巴矢量圖標庫」百度搜索第一位就是啦再次點擊即可取消,或者在暫存架中滑鼠滑過圖標,圖標右上角有刪除小按鈕也可刪除選中
  • 用純CSS畫一顆愛心
    話不多說,先教大家怎麼用css畫一個圓形。height: 100px; border:1px solid red; background-color: red; margin: 300px 0px 0px 0px; float: left; position: relative; right: 152px;}做完這些的效果已經基本上出來了,但是我們還需要調整一下愛心的角度,這時就需要用到我們css
  • 12個CSS高級技巧匯總
    6、對圖標使用SVG我們沒有理由不對圖標使用SVG:.logo {background: url("logo.svg");}SVG對所有的解析度類型都具有良好的擴展性,並支持所有瀏覽器都回歸到IE98、對純CSS滑塊使用 max-height使用 max-height 和溢出隱藏來實現只有CSS的滑塊:.slider ul {max-height: 0;overlow: hidden;}.slider:hover ul {
  • 原來css中的border還可以這樣玩
    前面的話:在看這篇文章之前你可能會覺得border只是簡單的繪製邊框,看了這篇文章,我相信你也會跟我一樣說一句「我靠,原來css中的
  • 網站前端ICONFONT圖標助力網站設計之WordPress應用
    以上是關於如何利用主題作者已經集成的ICO庫來添加圖標;如果有更多欄目,只需要尋找更多自己看中的圖標,然後按以上方法添加進CSS類即可;網站集成阿里ICONFONT圖標阿里ICO調用格式:第一種:icon單個使用單個圖標用戶可以自行選擇下載不同的格式使用,包括png,ai,svg。
  • 純CSS實現滑鼠放上去改變文字內容
    這篇文章主要介紹了純CSS實現滑鼠放上去改變文字內容,需要的朋友可以參考下:css樣式複製代碼 代碼如下:.remind span {display:block;margin-top:-22px;}.remind a:hover {padding-top:22px;}.remind
  • 圖標選擇組件 e-icon-picker 1.0.2 發布,修復 bug
    e-icon-picker 圖標選擇組件簡潔大方,專為element-ui和font-awesome圖標庫開發的圖標選擇組件,希望大家喜歡!e-icon-picker快速使用import iconPicker from 'e-icon-picker';import 'e-icon-picker/dist/index.css
  • CSS的親兒子,居然不是Less??
    前段時間給大家分享了用less插件來一步簡化代碼,需要回憶一下的同學,三金已經把文章連結準備好了喲~點擊藍色字體就可以「溫故知新」啦↓那麼今天,無憂老師給大家帶來了CSS擴展語言神奇,它就是——Sass跟上無憂老師,一起來看看這個「神器」的優秀之處吧~先來聊聊sass吧,之前用了很久的less,剛開始接觸的時候感覺這東西就是個神器
  • CSS樣式表和選擇器
    css的最新版本是css3,我們目前學習的是css2.1。因為css3和css2.1不矛盾,必須先學2.1然後學3。接下來我們要講一下為什麼要使用CSS。現在,html只提供數據和一些控制項,完全交給css提供各種各樣的樣式。CSS的重點知識點盒子模型、浮動、定位CSS 整體感知我們先來看一段簡單的css代碼:<!
  • 每個前端都需要知道這些面向未來的CSS技術
    這次筆者整理一些未來普及或者現在同學們可能已經用到的CSS特性,包括SVG圖標、滾動特性、CSS自定義屬性、CSS現代偽類 、JS in CSS、Web Layout、混合模式和濾鏡、CSS計數器等等。
  • 又一款開源的web圖標庫,優雅精緻可商用
    font-awesome算是用的最多的圖標庫吧。哈哈哈哈哈!現在又有新選擇了。而且這個庫的質量很高,簡潔優雅,圓潤的線條風格。項目是本月初上線的,上線後就迅速登上了github的趨勢榜,目前已經推出了300多個圖標了。項目基於MIT授權,這意味著可以免費商用了。
  • Iconfont - 設計師/前端開發者必備的圖標管理工具
    設計師將圖標上傳到 Iconfont,可以自定義下載多種格式的icon,也可將圖標轉換為字體,方便前端工程師自由調整與使用。通過這個免費的工具,設計師不僅可以瀏覽下載大量優秀設計師的圖標作品,還可以管理和展示自己設計的圖標。已經成為很多UI設計師和前端開發者日常工作的必備工具。
  • 12 個 CSS 高級技巧匯總
    ◆對圖標使用SVG我們沒有理由不對圖標使用SVG:.logo { background: url("logo.svg");}SVG對所有的解析度類型都具有良好的擴展性,並支持所有瀏覽器都回歸到IE9。這樣可以避開.png、.jpg或.gif文件了。