If you use @ionic/core with Vite, you've probably hit this at some point: your build output contains a svg/ folder with every single Ionic icon — all ~1,300 of them — even though your app only uses a dozen.
This happens because Ionic registers icons at runtime by name (<ion-icon name="add-outline" />), so bundlers have no idea which SVG files to keep and which to drop.
I built vite-plugin-ionic-icons to fix exactly that.
What it does
-
Build mode — scans your source files for every
<ion-icon name="...">usage across all frameworks, then emits only those icons as build assets. If you use 12 icons, you ship 12 SVG files. -
Dev mode — serves icons on-demand directly from
node_modulesvia a middleware. No copying, instant startup.
The difference in bundle output can be dramatic:
| Before | After | |
|---|---|---|
| SVG files emitted | ~1,300 | only what you use |
svg/ folder size |
~4 MB | a few KB |
Installation
npm install -D vite-plugin-ionic-icons
Requires
vite >= 4.0.0and@ionic/corealready installed.
Setup (zero config)
// vite.config.ts
import { defineConfig } from 'vite';
import ionicIcons from 'vite-plugin-ionic-icons';
export default defineConfig({
plugins: [
ionicIcons(),
],
});
That's it. In dev, icons are served at /svg/<name>.svg. In production, only the icons detected in your source are emitted to dist/svg/.
Framework support
The scanner recognizes icon usage across all major frameworks with a set of regex patterns tailored to each syntax.
React / JSX / TSX
<ion-icon name="add-outline" />
<ion-icon name="trash" size="small" />
Vue (static & literal dynamic binding)
<ion-icon name="add-outline" />
<ion-icon :name="'add-outline'" />
Angular (static & literal property binding)
<ion-icon name="add-outline"></ion-icon>
<ion-icon [name]="'add-outline'"></ion-icon>
Svelte
<ion-icon name="add-outline" />
Mithril / Preact h()
m('ion-icon', { name: 'add-outline' })
h('ion-icon', { name: 'add-outline' })
Plain HTML
<ion-icon name="add-outline"></ion-icon>
Dynamic icons
If you resolve icon names at runtime (from an API, user settings, etc.), the static scanner can't detect them. Just list them explicitly:
ionicIcons({
extraIcons: ['warning-outline', 'checkmark-circle', 'close-circle'],
})
All options
ionicIcons({
// Directory (or array of dirs) to scan. Default: './src'
srcDir: './src',
// File extensions to scan. Default: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte']
extensions: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte'],
// Icons to always include (for dynamic icon names).
extraIcons: ['warning-outline'],
// Where Ionic's SVGs live. Default: 'node_modules/@ionic/core/dist/ionic/svg'
iconSrcDir: 'node_modules/@ionic/core/dist/ionic/svg',
// Output sub-directory. Default: 'svg'
iconDestDir: 'svg',
// Log detected icons and emit count. Default: false
verbose: true,
})
How it works under the hood
Build time
└── generateBundle()
├── scan srcDir recursively
├── apply regex patterns (one per framework)
├── deduplicate icon names
└── emitFile() for each used icon → dist/svg/<name>.svg
Dev time
└── configureServer()
└── middleware: GET /svg/<name>.svg
└── pipe from node_modules/@ionic/core/dist/ionic/svg/<name>.svg
The scanner runs once per build/serve session and the result is cached — no repeated file-system work.
Links
Feedback and PRs are very welcome. If you use Ionic with Vite, give it a try and let me know how it goes!
Top comments (0)