Examples
Each example below is built by vite-svg-2-webfont when these docs are built. The Generated CSS tab shows the stylesheet the plugin produced, with font URLs shortened to file names; Vite rewrites them to the emitted assets. The Output tab renders the font with the example's real class names.
Basic icon set
Point context at a folder of SVGs: each file becomes a glyph and an icon-<name> class. The icons scale with font-size and take the text color.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
}),
],
});@font-face {
font-family: "iconfont";
font-display: block;
src: url("iconfont.eot") format("embedded-opentype"),
url("iconfont.woff") format("woff"),
url("iconfont.woff2") format("woff2"),
url("iconfont.ttf") format("truetype"),
url("iconfont.svg") format("svg");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: iconfont !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Custom naming
Choose the font name, the base class, and the icon class prefix to match your project's conventions.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'ui-icons',
classPrefix: 'ui-icon-',
baseSelector: '.ui-icon',
}),
],
});@font-face {
font-family: "ui-icons";
font-display: block;
src: url("ui-icons.eot") format("embedded-opentype"),
url("ui-icons.woff") format("woff"),
url("ui-icons.woff2") format("woff2"),
url("ui-icons.ttf") format("truetype"),
url("ui-icons.svg") format("svg");
}
.ui-icon {
line-height: 1;
}
.ui-icon:before {
font-family: ui-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.ui-icon-add:before {
content: "\f101";
}
.ui-icon-arrow-right:before {
content: "\f102";
}
.ui-icon-bell:before {
content: "\f103";
}
.ui-icon-check:before {
content: "\f104";
}
.ui-icon-close:before {
content: "\f105";
}
.ui-icon-download:before {
content: "\f106";
}
.ui-icon-flag:before {
content: "\f107";
}
.ui-icon-heart:before {
content: "\f108";
}
.ui-icon-home:before {
content: "\f109";
}
.ui-icon-logo:before {
content: "\f10a";
}
.ui-icon-search:before {
content: "\f10b";
}
.ui-icon-spinner:before {
content: "\f10c";
}
.ui-icon-star:before {
content: "\f10d";
}
.ui-icon-trash:before {
content: "\f10e";
}
.ui-icon-user:before {
content: "\f10f";
}
Ligatures
Ligatures are on by default: inside an element that uses the icon font, typing an icon's name renders its glyph, so the icon also has a readable text form.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'ligature-icons',
ligature: true,
}),
],
});@font-face {
font-family: "ligature-icons";
font-display: block;
src: url("ligature-icons.eot") format("embedded-opentype"),
url("ligature-icons.woff") format("woff"),
url("ligature-icons.woff2") format("woff2"),
url("ligature-icons.ttf") format("truetype"),
url("ligature-icons.svg") format("svg");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: ligature-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Type-safe icon component
The TypeScript virtual module exports a readonly icons tuple, an exact IconName union, iconBaseSelector, and iconClassPrefix. An <Icon> component only accepts names that exist. The same module works with Vue, React, Angular, and Svelte, independently of file output settings.
Add /// <reference types="vite-svg-2-webfont/virtuals" /> to your existing vite-env.d.ts, or add vite-svg-2-webfont/virtuals to compilerOptions.types in your application's tsconfig.json. Run Vite once before standalone type-checking on a fresh installation; starting Vite after a type-check step cannot initialize that earlier step. Reinstalling dependencies may remove generated types.
Declarations live inside the installed plugin package. Apps sharing that installation share declarations: use distinct tsModuleId values to avoid overwrites, but this does not isolate app types. Removed IDs remain until the generated directory is cleared (preserve its empty index.d.ts) or the package is reinstalled. Unwritable installations fail generation rather than silently serving stale types. A crashed publisher may leave a .lock file containing its PID and creation time; remove it only after all generators have stopped.
For optional physical source output, set generateFiles: 'ts' (default destination: <dest>/<fontName>.ts) or include 'ts' in the array. generateFiles: true includes TS only with explicit tsDest; false/omitted writes no TS source. Build output also requires allowWriteFilesInBuild: true. The metadata describes generation context, even with CSS disabled or a custom CSS template that renders different classes.
import { resolve } from 'node:path';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default {
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'typed-icons',
tsModuleId: 'typed-icons',
}),
],
};Choose either option, preserving any existing type references or entries:
/// <reference types="vite-svg-2-webfont/virtuals" />{
"compilerOptions": {
"types": ["vite/client", "vite-svg-2-webfont/virtuals"]
}
}<script setup lang="ts">
import { iconBaseSelector, iconClassPrefix, type IconName } from 'virtual:typed-icons';
defineProps<{ name: IconName }>();
</script>
<template>
<i :class="[iconBaseSelector.slice(1), `${iconClassPrefix}${name}`]" aria-hidden="true" />
</template>import { iconBaseSelector, iconClassPrefix, type IconName } from 'virtual:typed-icons';
export function Icon({ name }: { name: IconName }) {
return <i className={`${iconBaseSelector.slice(1)} ${iconClassPrefix}${name}`} aria-hidden="true" />;
}import { Component, computed, input } from '@angular/core';
import { iconBaseSelector, iconClassPrefix, type IconName } from 'virtual:typed-icons';
@Component({
selector: 'app-icon',
template: '<i [class]="classes()" aria-hidden="true"></i>',
})
export class IconComponent {
readonly name = input.required<IconName>();
protected readonly classes = computed(() => `${iconBaseSelector.slice(1)} ${iconClassPrefix}${this.name()}`);
}<script lang="ts">
import { iconBaseSelector, iconClassPrefix, type IconName } from 'virtual:typed-icons';
let { name }: { name: IconName } = $props();
</script>
<i class="{iconBaseSelector.slice(1)} {iconClassPrefix}{name}" aria-hidden="true"></i>@font-face {
font-family: "typed-icons";
font-display: block;
src: url("typed-icons.eot") format("embedded-opentype"),
url("typed-icons.woff") format("woff"),
url("typed-icons.woff2") format("woff2"),
url("typed-icons.ttf") format("truetype"),
url("typed-icons.svg") format("svg");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: typed-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
export const iconBaseSelector = ".icon" as const;
export const iconClassPrefix = "icon-" as const;
export const icons = [
"add",
"arrow-right",
"bell",
"check",
"close",
"download",
"flag",
"heart",
"home",
"logo",
"search",
"spinner",
"star",
"trash",
"user"
] as const;
export type IconName = (typeof icons)[number];Custom CSS template
Add your own utilities to the generated stylesheet by starting from the built-in template and extending it. This template adds a large size and a spinning animation.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'custom-template-icons',
cssTemplate: resolve(import.meta.dirname, 'icons.css.hbs'),
}),
],
});@font-face {
font-family: "custom-template-icons";
font-display: block;
src: url("custom-template-icons.eot") format("embedded-opentype"),
url("custom-template-icons.woff") format("woff"),
url("custom-template-icons.woff2") format("woff2"),
url("custom-template-icons.ttf") format("truetype"),
url("custom-template-icons.svg") format("svg");
}
.icon {
display: inline-block;
line-height: 1;
}
.icon:before {
font-family: custom-template-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
/* Utilities this template adds to the generated CSS. */
.icon--lg {
font-size: 2em;
}
.icon--spin {
animation: custom-template-icons-spin 1s linear infinite;
}
@keyframes custom-template-icons-spin {
to {
transform: rotate(360deg);
}
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Multi-weight family
Give each weight its own folder of matching file names. The designs share one font file, and a modifier class switches weight. bold/ has no heart.svg, so the fallback policy borrows it from the regular design.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, 'icons'),
fontName: 'weighted-icons',
variants: [
{ name: 'light', context: 'light', weight: 300 },
{ name: 'regular', context: 'regular', weight: 400, default: true },
{ name: 'bold', context: 'bold', weight: 700 },
],
missingGlyphs: { behavior: 'fallback', variant: 'regular' },
}),
],
});@font-face {
font-family: "weighted-icons";
font-display: block;
font-style: normal;
font-weight: 300;
src: url("weighted-icons.woff") format("woff"),
url("weighted-icons.woff2") format("woff2");
}
@font-face {
font-family: "weighted-icons";
font-display: block;
font-style: normal;
font-weight: 400;
src: url("weighted-icons.woff") format("woff"),
url("weighted-icons.woff2") format("woff2");
}
@font-face {
font-family: "weighted-icons";
font-display: block;
font-style: normal;
font-weight: 700;
src: url("weighted-icons.woff") format("woff"),
url("weighted-icons.woff2") format("woff2");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: weighted-icons !important;
font-style: normal;
font-weight: 400;
font-synthesis: none;
vertical-align: top;
}
:is(.icon).icon--light:before {
font-weight: 300 !important;
}
:is(.icon).icon--regular:before {
font-weight: 400 !important;
}
:is(.icon).icon--bold:before {
font-weight: 700 !important;
}
.icon-add:before {
content: "\f101";
}
.icon-check:before {
content: "\f102";
}
.icon-close:before {
content: "\f103";
}
.icon-heart:before {
content: "\f104";
}
.icon-search:before {
content: "\f105";
}
icon--light
default (regular)
icon--bold
Color glyphs
Keep the SVG colors for selected icons. Color glyphs need TTF, WOFF, or WOFF2 output; the other icons still follow the text color. Some platforms, such as Safari on macOS, show the single-color version instead.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'color-icons',
types: ['woff2'],
colorGlyphs: ['logo', 'flag'],
}),
],
});@font-face {
font-family: "color-icons";
font-display: block;
src: url("color-icons.woff2") format("woff2");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: color-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Theme colors in one glyph
Use one charging battery glyph with three paths in both light and dark themes:
- Primary: the casing uses
currentColor, following the element's text color (dark slate → light slate). - Secondary: the charge area uses
#059669, mapped to thesecondarypalette slot (emerald → mint). - Static: the lightning bolt uses
#fbbf24, staying amber in both themes.
CSS @font-palette-values overrides only the secondary slot, and font-palette selects the theme without loading a second font or switching glyphs.
The custom template reads colorSlots.secondary, the generated palette index. Slot indices follow ASCII name order and stay stable when icons change, including when a slot's source color is temporarily unused. Adding or renaming slots can change indices, so use the template mapping instead of hardcoding them.
import { resolve } from 'node:path';
import { defineConfig } from 'vite-plus';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, 'icons'),
fontName: 'theme-icons',
types: ['woff2'],
colorGlyphs: true,
colorSlots: { secondary: '#059669' },
cssTemplate: resolve(import.meta.dirname, 'icons.css.hbs'),
}),
],
});@font-face {
font-family: "theme-icons";
font-display: block;
src: url("theme-icons.woff2") format("woff2");
}
@font-palette-values --theme-icons-light {
font-family: "theme-icons";
override-colors: 0 #059669;
}
@font-palette-values --theme-icons-dark {
font-family: "theme-icons";
override-colors: 0 #34d399;
}
.icon {
display: inline-block;
font-size: 64px;
line-height: 1;
color: var(--primary-color, #334155);
font-palette: --theme-icons-light;
}
.icon:before {
font-family: "theme-icons" !important;
font-style: normal;
font-weight: normal !important;
}
/* In an app, use your theme selector, e.g. [data-theme="dark"] .icon. */
.icon.theme-dark {
--primary-color: #94a3b8;
font-palette: --theme-icons-dark;
}
.icon-battery:before {
content: "\f101";
}
Light: dark slate casing · emerald charge · amber bolt
Dark: light slate casing · mint charge · amber bolt
Palette overrides use explicit colors, not per-element var(--secondary-color) or currentColor. Select predefined palettes for your themes; only the foreground follows an inherited CSS variable here. Identical resolved RGB fills share a slot across selected icons and variants, with their layer opacity retained. Slots do not affect monochrome icons. Platforms using the monochrome fallback lose the secondary color; see color compatibility.
Pinned codepoints
Pin the codepoints of icons you publish, so CSS or content that refers to a codepoint keeps working when icons are added. Unpinned icons get the next free codepoint.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'pinned-icons',
codepoints: {
add: 0xe001,
search: 0xe002,
star: 0xe003,
},
}),
],
});@font-face {
font-family: "pinned-icons";
font-display: block;
src: url("pinned-icons.eot") format("embedded-opentype"),
url("pinned-icons.woff") format("woff"),
url("pinned-icons.woff2") format("woff2"),
url("pinned-icons.ttf") format("truetype"),
url("pinned-icons.svg") format("svg");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: pinned-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\e001";
}
.icon-search:before {
content: "\e002";
}
.icon-star:before {
content: "\e003";
}
.icon-arrow-right:before {
content: "\f101";
}
.icon-bell:before {
content: "\f102";
}
.icon-check:before {
content: "\f103";
}
.icon-close:before {
content: "\f104";
}
.icon-download:before {
content: "\f105";
}
.icon-flag:before {
content: "\f106";
}
.icon-heart:before {
content: "\f107";
}
.icon-home:before {
content: "\f108";
}
.icon-logo:before {
content: "\f109";
}
.icon-spinner:before {
content: "\f10a";
}
.icon-trash:before {
content: "\f10b";
}
.icon-user:before {
content: "\f10c";
}
Inline fonts
Embed the fonts in the generated CSS as data URLs. The page makes no separate font requests, at the cost of a larger stylesheet.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'inline-icons',
types: ['woff2'],
inline: true,
}),
],
});@font-face {
font-family: "inline-icons";
font-display: block;
src: url("data:font/woff2;charset=utf-8;base64,…") format("woff2");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: inline-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Modern formats and preload
Generate only the formats current browsers need, and preload WOFF2 so the browser starts downloading it before the stylesheet asks for it. Preload tags are added to the built HTML only.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'preloaded-icons',
types: ['woff2', 'woff'],
preloadFormats: ['woff2'],
}),
],
});<link rel="preload" href="/assets/preloaded-icons-[hash].woff2" as="font" type="font/woff2" crossorigin />@font-face {
font-family: "preloaded-icons";
font-display: block;
src: url("preloaded-icons.woff2") format("woff2"),
url("preloaded-icons.woff") format("woff");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: preloaded-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}
Writing files for other tools
Write the fonts, CSS, and HTML preview to a folder as well, for tools outside Vite. These files are extra copies; the app still uses the fonts Vite bundles.
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import viteSvgToWebfont from 'vite-svg-2-webfont';
export default defineConfig({
plugins: [
viteSvgToWebfont({
context: resolve(import.meta.dirname, '../icons'),
fontName: 'written-icons',
types: ['woff2', 'woff'],
dest: resolve(import.meta.dirname, 'generated'),
generateFiles: true,
allowWriteFilesInBuild: true,
}),
],
});generated/
├── written-icons.css
├── written-icons.html
├── written-icons.woff
└── written-icons.woff2@font-face {
font-family: "written-icons";
font-display: block;
src: url("written-icons.woff2") format("woff2"),
url("written-icons.woff") format("woff");
}
.icon {
line-height: 1;
}
.icon:before {
font-family: written-icons !important;
font-style: normal;
font-weight: normal !important;
vertical-align: top;
}
.icon-add:before {
content: "\f101";
}
.icon-arrow-right:before {
content: "\f102";
}
.icon-bell:before {
content: "\f103";
}
.icon-check:before {
content: "\f104";
}
.icon-close:before {
content: "\f105";
}
.icon-download:before {
content: "\f106";
}
.icon-flag:before {
content: "\f107";
}
.icon-heart:before {
content: "\f108";
}
.icon-home:before {
content: "\f109";
}
.icon-logo:before {
content: "\f10a";
}
.icon-search:before {
content: "\f10b";
}
.icon-spinner:before {
content: "\f10c";
}
.icon-star:before {
content: "\f10d";
}
.icon-trash:before {
content: "\f10e";
}
.icon-user:before {
content: "\f10f";
}