About one year ago, I wrote a blog post Journey with Icons, sharing the tools I have made for solving my needs on using icons in frontend projects.
During this period, the Vite along its community has evolved quite a lot. The mindsets of Vite have inspired many projects to come up with efficient and innovative solutions.
In this post, I will share the continuation of my journey with icons and the tools I have ended up with so far.
PurgeIcons & Its Limitations #
PurgeIcons is my first attempt to improve the loading speed of Iconify - a united icon library that allows you to use any icons for any framework. The main problem is that it’s purely client-side. Even it’s flexible to work with any framework, the client-side requests inevitably introduce the flash of missing icons. To solve that, I made PurgeIcons by statically scanning your icon usages and bundle them together with your app, so the Iconify runtime could load them without additional requests.
This solution works, but it only solves the problem partially. As the icons are bundled within JavaScript and functions outside the frameworks, it’s not ideal for working with framework-specific features like server-side rendering/generation, props passing, etc. We need to find a better way of doing it.
The New Solution #
One of the core-concept of Vite is that everything is on-demand. Modules get transpiled only when they are being requested. In this way, the Vite server starts immediately without the need to bundle your entire app. Additionally, Vite’s plugin API is an extension on top of Rollup’s plugin system, which allows you to do some custom transformations to the modules.
So, if we think in Vite’s way - maybe we could solve this at compile-time instead of client-side! By using virtual modules, I was able to serve the icons as components on-the-fly and made it as vite-plugin-icons
(renamed to unplugin-icons
later on).
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
IconsPlugin()
]
})
function IconsPlugin() {
return {
name: 'vite-plugin-icons',
// tell Vite that ids start with `~icons/` are virtual files
resolveId(id) {
if (id.startsWith('~icons/'))
return id
return null
},
// custom logic to load the module
load(id) {
if (!id.startsWith('~icons/'))
return
const [prefix, collection, name] = id.split('/')
// get icon data from Iconify
const svg = getIconSVG(collection, name)
// we compile the SVG as a Vue component
return Vue3Compiler(svg)
}
}
}
And the usage will be like this:
<script setup>
import MdiAlarm from '~icons/mdi/alarm'
import FaBeer from '~icons/fa/beer'
import TearsOfJoy from '~/icons/twemoji/face-with-tears-of-joy'
</script>
<template>
<MdiAlarm />
<FaBeer style="color: orange" />
<TearsOfJoy />
</template>
You might notice the usages are pretty similar to existing solutions like React Icons. However, most of them approaching this by compiling all the icons into multiple files and distribute them as npm packages. Not only does it ships additional bytes for every icon and increases the time for compilers to parsing them, that also means you are limited to what they have offered exclusively.
With unplugin-icons
, you can use any icons available in Iconify (which is 100+ icon sets with over 10,000 icons and continue growing) by the following convention:
import Icon from '~icons/[collection]/[name]'
You can learn more about the installation and usage on unplugin-icons
Universal #
Universal on Icons #
The unification or Icons are already done in Iconify by providing the icons in the same, normalized JSON format, so what if we could have it more universally available for the tools we loved?
iconify/collections-jsonUniversal on Frameworks #
Initially, I was made this plugin only for Vue 3 on Vite. But since we are doing the complication on-demand, I figured out that we could actually apply for different compilers based on the frameworks users use. With that idea, now it supports using icons as components for Vue 3, Vue 2, React, Preact and Solid! (Contributions to add more is great welcome!)
function Vue3Compiler(svg) { /* ... */ }
function Vue2Compiler(svg) { /* ... */ }
function JSXCompiler(svg) { /* ... */ }
function SolidCompiler(svg) { /* ... */ }
// ...add more!
function IconsPlugin({ compiler }) {
return {
name: 'vite-plugin-icons',
resolveId(id) { /* ... */ },
load(id) {
/* ... */
// we could apply different compilers here as needed
return compiler(SVG)
}
}
}
With this, you can have it working in React like:
import MdiAlarm from '~icons/mdi/alarm'
import FaBeer from '~icons/fa/beer'
import TearsOfJoy from '~/icons/twemoji/face-with-tears-of-joy'
export function MyComponent() {
return (
<>
<MdiAlarm />
<FaBeer style="color: orange" />
<TearsOfJoy />
</>
)
}
Universal on Build Tools #
In the past few weeks, I have joined NuxtLabs and worked on a universal plugin layer for our various bundling tools - unjs/unplugin. It allows you to use a unified plugin API to write plugins for Vite, Webpack, Rollup, Nuxt, Vue CLI, and more only once. To make it work, all we need to do is to change our code like:
export function VitePluginIcons() {
return {
name: 'vite-plugin-icons',
resolveId(id) { /* ... */ },
load(id) { /* ... */ }
}
}
import { createUnplugin } from 'unplugin'
const unplugin = createUnplugin(() => {
return {
name: 'unplugin-icons',
resolveId(id) { /* ... */ },
load(id) { /* ... */ }
}
})
// Use unplugin to generate plugins for different build tools
export const VitePluginIcons = unplugin.vite
export const WebpackPluginIcons = unplugin.webpack
export const RollupPluginIcons = unplugin.rollup
That’s cool. With it, you don’t need to learn each frameworks’ plugin API and publish them in multiple packages - now you got one package for all of them!
unjs/unpluginUniversal Solution #
With all the effort above, I converted my vite-plugin-icons
, a Vite + Vue 3 specific icon plugin, to unplugin-icons
as a universal icons solution.
For what I mean universal, I mean literally, you can use:
- Vue 3 + Vite + Carbon Icons
- React + Next.js + Material Design Icons
- Vue 2 + Nuxt.js + Unicons
- React + Webpack + Twemoji
- Solid + Vite + Tabler
- Vanila + Rollup + BoxIcons
- Web Components + Vite + Ant Design Icons
- Svelte + SvelteKit + EOS Icons
- Any + Any + Any
…really, you made the combinations!
Get it now 👇
unplugin-iconsOne More Thing #
Oh, you are still here. So I guess you are looking for something even further.
As you might notice, whenever you want to use an icon, you need to import it first, name it, and then use it. In this case, the icon name has been repeated at least three times. For example:
Vue #
<script setup>
import MdiAlarm from '~icons/mdi/alarm'
</script>
<template>
<MdiAlarm />
</template>
React #
import MdiAlarm from '~icons/mdi/alarm'
export function MyComponent() {
return (
<div>
<MdiAlarm />
</div>
)
}
So yes, we might need a better way to do this.
Auto-importing #
Inspired by nuxt/components which registers components under your ./components
directory automatically, I made unplugin-vue-components (yes, another unplugin!) do to compile-time components auto-importing on-demand. With the on-demand natural, we could even make the components resolving on-demand. What a perfect complement for our icon solution!
unplugin-vue-components
provide the options resolvers
to provide custom functions to resolve where the components should be imported from.
Here is an example configuration for Vite (since both of them are unplugins, you can also use them for Webpack and Rollup):
// vite.config.js
import { defineConfig } from 'vite'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'
export default defineConfig({
plugins: [
/* ... */
Icons(),
Components({
resolvers: [
IconsResolver({
// to avoid naming conflicts
// a prefix can be specified for icons
prefix: 'i'
})
]
})
]
})
Then we can use them directly in our templates, no more imports and repeats (and you can change the icons much easier as you don’t need to update in three places):
<template>
<!-- both PascalCase and dash-case are supported by Vue -->
<IMdiAlarm />
<i-fa-beer style="color: orange" />
</template>
Isn’t it perfect?!
Learn more: unplugin-vue-components
Auto-import integrations for
@nuxt/components
is in progress.
Auto-importing for JSX #
Oh, I almost forgot about it. Since JSX is more like plain JavaScript in some ways and JSX components are just functions or classes, the thing is actually a bit simpler. For that, we can use another unplugin I made - unplugin-auto-import.
For some background here, unplugin-auto-import
is a compile-time successor of vue-global-api to improve DX of Vue Composition API (directly use of ref
, computed
, etc.).
With the expansion to a general auto-importing solution for any API sets, it’s also possible to do auto-importing for JSX components. in unplugin-auto-import
, we implement the same resolver interface for it.
// vite.config.js
import { defineConfig } from 'vite'
import Icons from 'unplugin-icons/vite'
import AutoImport from 'unplugin-auto-import/vite'
import IconsResolver from 'unplugin-icons/resolver'
export default defineConfig({
plugins: [
/* ... */
Icons({
compiler: 'jsx'
}),
AutoImport({
imports: [
'react' // preset for react
],
resolvers: [
IconsResolver({
prefix: 'Icon',
extension: 'jsx'
})
]
})
]
})
Here is your React component, and you are welcome :)
export function MyComponent() {
return (
<>
<IconMdiAlarm />
<IconFaBeer style="color: orange" />
</>
)
}
Recap #
For a quick summary, here is the list of projects mentioned for these solutions:
- unjs/unplugin - Unified plugin system for Vite, Rollup, Webpack, and more.
- unplugin-icons - Access thousands of icons as components on-demand.
- unplugin-vue-components - On-demand components auto importing.
- unplugin-auto-import - Auto import APIs on-demand.
Meanwhile, you might also find these tools from my last journey helpful:
- icones - Icon Explorer for Iconify with Instant searching and exporting.
- vscode-iconify - Iconify IntelliSense for VS Code.
If you enjoy them, you might also want to check my Vue + Vite starter template with them configured in-box.
- vitesse - Opinionated Vite Starter Template.
- vitesse-lite - Lightweight version of Vitesse.
- vitesse-webext - WebExtension Vite Starter Template.
- vitesse-nuxt - Vitesse experience for Nuxt 2 and Vue 2.
Again, thanks for reading through :)