# unplugin-icons: Iconify icons as on-demand components in Vite, Webpack and Nuxt

> unplugin-icons turns the ~icons/{collection}/{icon} import convention into real components across React, Vue, Svelte and more, bundling only the icons you use. It is a build-time compiler, not a runtime library, and that shapes both its strengths and its limits.

**unplugin/unplugin-icons** — 🤹 Access thousands of icons as components on-demand universally.

- Repository: https://github.com/unplugin/unplugin-icons
- Website: https://www.npmjs.com/package/unplugin-icons
- Stars: 4,942 · Forks: 161
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/unplugin-unplugin-icons

## The problem unplugin-icons solves: icon sets that ship as components

Icon libraries usually arrive in one of two shapes. Either you install a package that exports every icon as a component, which means your bundler has to reason about thousands of exports, or you copy SVG markup into your source and lose the ability to change size and colour with CSS. unplugin-icons takes a third path. It reads Iconify data at build time, compiles the specific icons you import, and hands your framework a normal component.

The README describes the scope as "~150 popular sets with over 200,000 icons, logos, emojis, etc." and says the data comes from Iconify. The import convention is `~icons/{collection}/{icon}`, so `~icons/carbon/accessibility` resolves to the Carbon set's accessibility icon and `~icons/mdi/account-box` to Material Design Icons. That string is not a runtime lookup. It is a virtual module the plugin creates during the build.

The intended audience is frontend teams with an existing bundler setup. The README lists Vite, Webpack, Rollup, Nuxt, Rspack and Vue CLI, and frameworks from Vanilla and Web Components through React, Vue 3, Vue Vapor, Solid and Svelte. If you are not using one of those build tools, nothing here applies to you.

## How the virtual module and compiler pipeline work

The mechanism is an unplugin transform. When your bundler encounters an import path starting with `~icons/`, the plugin intercepts it, parses the collection and icon name out of the path, looks up the SVG body in the installed Iconify data, and emits a module that your framework can consume. The README describes this as "on-demand", and notes that with the full `@iconify/json` package installed, "Only icons you actually use will be bundled in production." The 120MB figure applies to what sits in node_modules, not to your output bundle.

The second stage is compilation. The repository has a `src/core/compilers` directory, and the README invites contributions there, which tells you compilation is per-framework rather than a single generic output. Each compiler emits idiomatic code for its target: JSX for React and Preact, a Vue SFC for Vue, and so on. You select it either implicitly through the importer's file extension or explicitly with the `compiler` option.

TypeScript support is handled through the `types/` directory and the subpath exports in package.json. There are dedicated type entry points for astro, ember, preact, qwik, raw, react, solid, svelte, svelte3, svelte4, svelte5 and vue. Those are separate declarations, not one shared shim, which matters if you write a mixed-framework monorepo.

One structural consequence: because this is a build-time transform, the set of icons in your bundle is fixed at build time. There is no documented runtime API for fetching an icon by name from a variable.

## Installing unplugin-icons and rendering a first icon

The README states the package is ESM-only and requires ES modules, meaning `"type": "module"` in package.json or `.mjs` file extensions. From v24.0.0 it also requires Node 20 or above, because unplugin v3.0.0 does. Install the plugin as a devDependency:

```bash
npm i -D unplugin-icons
```

Then install the icon data. The README gives three options. The full collection is recommended for flexibility and installs all sets, roughly 120MB on disk:

```bash
npm i -D @iconify/json
```

If you prefer to keep node_modules small, install only the sets you need. The README uses mdi and carbon as its example:

```bash
npm i -D @iconify-json/mdi @iconify-json/carbon
```

Register the plugin in your bundler config. This is the Vite form:

```ts
// vite.config.ts
import Icons from 'unplugin-icons/vite'

export default defineConfig({
  plugins: [
    Icons({ /* options */ }),
  ],
})
```

Now import an icon by convention and render it. The README's React example:

```jsx
import IconAccessibility from '~icons/carbon/accessibility'
import IconAccountBox from '~icons/mdi/account-box'

function App() {
  return (
    <div>
      <IconAccessibility />
      <IconAccountBox style={{ fontSize: '2em', color: 'red' }} />
    </div>
  )
}
```

What you should see: the two icons render inline in the page, and because the plugin compiles them at build time they are present in the server-rendered HTML rather than appearing after hydration. The README calls this "SSR / SSG friendly" and says it avoids FOUC. Styling works through normal CSS, so `style={{ fontSize: '2em', color: 'red' }}` is all it takes to resize and recolour.

## Auto-importing icons and the resolver path

Writing an import line for every icon gets tedious, and the README offers two ways around it. The first is the resolver, which plugs into unplugin-vue-components so that `<i-carbon-accessibility />` in a template resolves without an explicit import. The Nuxt configuration in the README shows the shape:

```ts
import IconsResolver from 'unplugin-icons/resolver'
import ViteComponents from 'unplugin-vue-components/vite'

export default defineNuxtConfig({
  modules: [
    'unplugin-icons/nuxt',
  ],
  vite: {
    plugins: [
      ViteComponents({
        resolvers: [
          IconsResolver({/* options */}),
        ],
      }),
    ],
  },
})
```

Note that this requires two packages working together. unplugin-icons supplies the resolver and the virtual modules; unplugin-vue-components supplies the scanning that turns an unimported tag name into an import. Neither replaces the other.

The second path is the experimental auto-install option, which fetches icon sets on demand when you import them rather than requiring a prior install step:

```ts
Icons({
  autoInstall: true, // Auto-detects npm/yarn/pnpm
})
```

The README labels this experimental, and the comment confirms it shells out to whichever package manager it detects. That is convenient in a scratch project and a liability in CI, where a build that silently installs packages is harder to reproduce. For anything you intend to ship, the explicit install is the safer default.

## Where unplugin-icons is the wrong tool

The build-time design has a hard boundary. If the icon name is only known at runtime, for example a dashboard where an administrator picks an icon per menu item and the value comes from a database, an import path cannot express it. The plugin only sees static import specifiers. You would need a different approach for that case, such as rendering SVG from Iconify's runtime API or storing the SVG body itself.

The ESM-only constraint is the second boundary. Projects still on CommonJS, or on a Vue CLI setup older than `@vue/cli-service ^5.0.8`, cannot use the plugin as documented. The README is explicit that Vue CLI users need `vue.config.mjs` with ES module syntax. That is a real migration cost if your config is a `.js` file today.

The Node version floor is the third. From v24.0.0, Node 20 or above is required. Teams pinned to Node 18 need to either upgrade or stay on the v23 line, which the release history shows was published on 2026-01-14.

Finally, the full `@iconify/json` package is around 120MB in node_modules. That does not affect your production bundle, but it does affect install time, CI cache size and Docker layer size. If any of those matter, install individual `@iconify-json/*` sets instead. The trade-off is that adding a new icon collection later means another install and another dependency change.

## unplugin-icons versus using Iconify directly

The obvious comparison is with Iconify's own runtime packages. Iconify's approach is to ship an icon component that resolves names at runtime, fetching icon data from a CDN or from an installed data package. That makes dynamic names trivial, since the name is just a prop. The cost is a runtime dependency, a loading state for icons that are not yet in the bundle, and the FOUC problem that unplugin-icons exists to avoid.

unplugin-icons inverts the trade. Everything is resolved at build time, so there is no runtime lookup, no loading state and no flash of unstyled content on a server-rendered page. The price is that the icon set is frozen at build time. The README frames this as "on-demand" bundling, and that framing is accurate for the static case.

A second comparison is with a conventional component library such as a hand-rolled SVG set or a framework-specific icon package. Those give you a fixed catalogue with predictable tree-shaking. unplugin-icons gives you roughly 150 sets through one import convention and lets you mix sets in the same file, which is why the README's React example imports from both carbon and mdi in four lines. If you only ever need one set and never switch, the difference is smaller than the feature list suggests.

## Maintenance, licensing and upgrade cost

The repository is not archived, and the last push was on 2026-09-11. The v24.0.0 release landed the same day, after v23.0.1 and v23.0.0 on 2026-01-14. That is a roughly eight-month gap between the v23 line and v24, so the project moves in occasional larger releases rather than continuous churn. Plan upgrades around major versions rather than expecting frequent patches.

The v24.0.0 upgrade carries a concrete cost: the Node 20 requirement inherited from unplugin v3.0.0. If your CI image or local runtime is older, the upgrade is blocked until that changes. The ESM-only requirement has been in place longer and is documented as a note at the top of the installation section, so most existing users have already dealt with it.

Licensing is MIT, stated in both the repository LICENSE file and the package.json license field. That permits commercial use and modification with the licence and copyright notice retained. The icon sets themselves are a separate matter: unplugin-icons fetches data from Iconify, and the README does not describe the licence of each individual collection. Icon sets commonly carry their own attribution requirements, so check the specific collection you ship rather than assuming the MIT licence of the plugin covers the artwork. This is a factual boundary, not legal advice.

On the toolchain side, the package uses pnpm (packageManager pnpm@12.3.4) and tsdown for its build. That only matters if you intend to contribute; consumers install the published package and never touch those.

## Conclusion

unplugin-icons is a good fit for teams already running Vite, Webpack, Rollup, Rspack or Nuxt who want Iconify's catalogue as tree-shaken components with SSR-friendly output. It is the wrong tool if you need runtime icon selection from a CMS value, or if your toolchain cannot move to ESM and Node 20 or above. Verify two things before adopting: that your package.json has "type": "module" (or your config uses .mjs), and that the icon set you plan to use is installed as a devDependency, either @iconify/json or a specific @iconify-json/* package. Check the compiler option against your framework, because Svelte needs compiler: 'svelte' while React and Vue infer it from the file extension.

## FAQ

### What is the difference between unplugin-icons and Iconify?

unplugin-icons uses Iconify as its icon data source and compiles icons into components at build time, so only the icons you import end up in the bundle. Iconify's own runtime packages resolve icon names at runtime instead. The README describes the plugin as "Powered by Iconify" and lists roughly 150 sets and over 200,000 icons.

### What are 10 different types of icons?

The README does not enumerate icon categories. It describes the catalogue as "~150 popular sets with over 200,000 icons, logos, emojis, etc." and points readers to https://icones.js.org/ for browsing.

### How do I change the icon symbol?

The README does not document a symbol-setting option. What it does document is picking a different icon by changing the import path, for example from ~icons/carbon/accessibility to ~icons/mdi/account-box, and styling the rendered component with normal CSS such as fontSize and color.

### Which is the best icon library?

The README does not rank icon libraries. It states that unplugin-icons draws from Iconify, which it says supports 100+ icon sets, and that you can mix collections freely in one file, as its React example does with carbon and mdi.

## Sources

- [License: MIT](https://github.com/unplugin/unplugin-icons/blob/main/LICENSE)
- [Project website](https://www.npmjs.com/package/unplugin-icons)
- [README](https://github.com/unplugin/unplugin-icons/blob/main/README.md)
- [Releases](https://github.com/unplugin/unplugin-icons/releases)
- [unplugin/unplugin-icons on GitHub](https://github.com/unplugin/unplugin-icons)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/unplugin-unplugin-icons
