Frameworks

Icons in Vue 3

Every glyphfox icon is a Vue 3 component with the same props as every other glyphfox package. This guide covers the install, props in templates, icons by name, morphing, global registration, TypeScript and the mistakes that leave an icon empty.

Install

terminal
npm install glyphfox

glyphfox has no dependencies. Vue 3.2 or newer is an optional peer dependency, only needed when you import glyphfox/vue. The same package also holds the React, Svelte and web component builds, so there is nothing else to install.

Each icon is a component named after the icon in PascalCase with an Icon suffix: arrow-right becomes ArrowRightIcon, x becomes XIcon. The icon browser shows the name of every icon and copies the Vue snippet for you.

Use the components

Toolbar.vue
<script setup>
import { HomeIcon, SearchIcon, BellIcon } from 'glyphfox/vue';

defineProps({ unread: { type: Number, default: 0 } });
</script>

<template>
  <nav class="toolbar">
    <a href="/" aria-label="Home"><HomeIcon /></a>
    <SearchIcon :size="20" :weight="1.5" class="muted" />
    <BellIcon variant="duotone" secondary-color="#ff7a1a" :modifier="unread ? 'dot' : undefined" />
  </nav>
</template>

Props are declared in camelCase, and Vue accepts both spellings in templates: secondary-color and secondaryColor reach the same prop. Bind numbers with :size="20"; a plain size="20" also works, because size, weight, rotate, duration and the other numeric props accept strings.

The components do not declare class, style or listeners, so Vue passes them through to the root <svg>. Your classes are merged with the built in glyphfox glyphfox-NAME classes, and @click, aria-*, data-* and id land on the svg itself.

Strokes use currentColor, so an icon takes the CSS color of its parent and follows hover states and themes for free. The color prop sets the color on the svg when you need a fixed one. The svg gets width and height attributes from size (24 by default); CSS width and height on a class override them.

Weight, corners and style

Three props change the drawing itself rather than the box around it: weight sets the stroke from 1 to 3, corners switches between round and sharp joins, and variant picks outline, duotone or solid. Below 24 px the stroke is thickened slightly so small icons do not look faint; absolute-weight keeps the exact stroke you asked for.

Axes.vue
<script setup>
import { FolderIcon } from 'glyphfox/vue';
</script>

<template>
  <FolderIcon :weight="1" />
  <FolderIcon :weight="3" corners="sharp" />
  <FolderIcon variant="duotone" secondary-color="#ff7a1a" />
  <FolderIcon variant="solid" />
  <FolderIcon :size="16" :weight="1.5" absolute-weight />
</template>

The solid style is generated at build time from the closed shapes of each icon, with inner lines cut out as negative space. Icons without closed shapes keep their outline when you ask for solid.

Icons by name, modifiers and fusion

Icon renders any spec of the name syntax: a plain name, a modifier badge (file+check, with @tl, @tr, @bl or @br for the corner), a fusion (cloud&lock:overlap) or a style (heart/solid). Fusion takes a list of names and a layout. Both are useful when the icon comes from data, such as a menu definition, a status field or a CMS.

Components such as FileIcon bring their own data. A name inside a string has no import behind it, so register the data once with registerIcons. The registry is shared by Icon, Fusion, MorphIcon and every morph-to prop, so one module covers the whole app. Modifier badges are drawn by the engine and need no registration.

src/icons.js
import { registerIcons } from 'glyphfox/vue';
import file from 'glyphfox/icons/file';
import cloud from 'glyphfox/icons/cloud';
import lock from 'glyphfox/icons/lock';
import menu from 'glyphfox/icons/menu';
import x from 'glyphfox/icons/x';
import pause from 'glyphfox/icons/pause';

// Every name used in a string: Icon, Fusion, MorphIcon and morph-to.
registerIcons([file, cloud, lock, menu, x, pause]);
src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import './icons.js';

createApp(App).mount('#app');
FileStatus.vue
<script setup>
import { Icon, Fusion } from 'glyphfox/vue';

defineProps({ saved: Boolean });
</script>

<template>
  <Icon :name="saved ? 'file+check' : 'file'" />
  <Icon name="file+alert@tr" variant="duotone" />
  <Fusion :icons="['cloud', 'lock']" layout="overlap" :gap="1" />
</template>

A fusion is a real icon: it follows weight, variant and every other prop, and where the parts overlap the part behind is cut around the part in front. Layouts are overlap, side, stack, grid, orbit, badge and inside; scale and offsets fine tune the parts.

Morph between icons

There are three ways to morph. MorphIcon animates whenever its name changes. Any icon component takes morph-to with a trigger (mount, hover, click or state). And the useMorph composable returns the animated frames for your own rendering.

MenuButton.vue
<script setup>
import { ref } from 'vue';
import { MorphIcon } from 'glyphfox/vue';

const open = ref(false);
</script>

<template>
  <button type="button" aria-label="Menu" :aria-expanded="open" @click="open = !open">
    <MorphIcon :name="open ? 'x' : 'menu'" :duration="300" />
  </button>
</template>
Triggers.vue
<script setup>
import { HeartIcon, PlayIcon } from 'glyphfox/vue';

defineProps({ playing: Boolean });
</script>

<template>
  <!-- toggles on every click -->
  <HeartIcon morph-to="heart/solid" trigger="click" />
  <!-- shows pause while playing is true -->
  <PlayIcon morph-to="pause" trigger="state" :active="playing" />
</template>

useMorph accepts a ref, a getter or a plain value and returns a computed { icon, variant } frame that animates whenever the spec changes. Pass the frame to Icon through its icon prop, or hand it to your own renderer.

PlayToggle.vue
<script setup>
import { ref } from 'vue';
import { Icon, useMorph } from 'glyphfox/vue';

const playing = ref(false);
const frame = useMorph(() => (playing.value ? 'pause' : 'play'), { duration: 250 });
</script>

<template>
  <button type="button" aria-label="Play" :aria-pressed="playing" @click="playing = !playing">
    <Icon v-if="frame" :icon="frame.icon" :variant="frame.variant" />
  </button>
</template>

Hand tuned pairs make common transitions feel designed: menu to x, play to pause, plus to check, sun to moon, eye to eye-off, bell to bell-ring, heart to heart/solid, copy to check, lock to unlock and wifi to wifi-off. Any other pair still morphs with an automatic match. The animated icons guide covers durations, delays and reduced motion.

Living states

The animate prop adds a preset that understands the icon: draw traces the strokes in drawing order, shake swings from the top, and pop, wiggle, spin, pulse and bounce do what they say. The animation is a small stylesheet inside the svg, so it needs no CSS import and it stops for visitors who prefer reduced motion.

Inbox.vue
<script setup>
import { BellIcon, LoaderCircleIcon, SparklesIcon } from 'glyphfox/vue';

defineProps({ hasNews: Boolean, loading: Boolean });
</script>

<template>
  <BellIcon animate="shake" trigger="state" :active="hasNews" />
  <LoaderCircleIcon v-if="loading" animate="spin" />
  <SparklesIcon animate="draw" trigger="hover" />
</template>

Global registration

Importing icons where you use them keeps each bundle small. When a design system needs a fixed set in every template, register those components once with app.component. Give Icon a distinct name such as GfIcon, so it cannot collide with another global component.

src/main.js
import { createApp } from 'vue';
import { HomeIcon, SearchIcon, SettingsIcon, Icon } from 'glyphfox/vue';
import App from './App.vue';
import './icons.js';

const app = createApp(App);
app.component('HomeIcon', HomeIcon);
app.component('SearchIcon', SearchIcon);
app.component('SettingsIcon', SettingsIcon);
app.component('GfIcon', Icon);
app.mount('#app');

With TypeScript, declare the global components so templates are checked:

src/glyphfox.d.ts
import type { HomeIcon, SearchIcon, SettingsIcon, Icon } from 'glyphfox/vue';

declare module 'vue' {
  interface GlobalComponents {
    HomeIcon: typeof HomeIcon;
    SearchIcon: typeof SearchIcon;
    SettingsIcon: typeof SettingsIcon;
    GfIcon: typeof Icon;
  }
}

export {};

TypeScript

Every component is typed as GlyphComponent with the shared props, so editors complete variant, modifier, animate and the rest. Icon specs are plain strings in the Vue types, because Vue prop inference cannot expand the full union of icon names. When you keep names in your own code, type them with IconName from glyphfox to catch typos at compile time.

src/nav.ts
import type { IconName } from 'glyphfox';

export const nav: { label: string; to: string; icon: IconName }[] = [
  { label: 'Home', to: '/', icon: 'home' },
  { label: 'Search', to: '/search', icon: 'search' },
  { label: 'Settings', to: '/settings', icon: 'settings' },
];

Deep imports and bundle size

Named imports from glyphfox/vue are tree shaken: the icon modules are marked free of side effects and each component is created with a pure call. In a Vite 8 production build (with Vue itself excluded), one icon component and the shared runtime come to about 53 KB minified, 20 KB gzip, and nine more icons add less than 1 KB gzip. The runtime carries the renderer, morph, fusion and modifier engines and is paid once.

Logo.vue
<script setup>
// One module, for tools that do not tree shake.
import { FoxIcon } from 'glyphfox/vue/icons/fox';
</script>

<template>
  <FoxIcon :size="32" variant="duotone" title="glyphfox" />
</template>

The tree shaking guide explains what keeps the set out of your bundle and what pulls it back in.

Props

Every icon component, Icon, Fusion and MorphIcon takes the shared props. In templates you can write the kebab-case attribute from the second column.

PropIn templatesTypeDefaultDescription
sizesizenumber, string24Width and height; numbers are pixels.
colorcolorstringcurrentColorMain color.
secondaryColorsecondary-colorstringcolorColor of the duotone layer and of the solid fill.
secondaryOpacitysecondary-opacitynumber0.3 / 1Opacity of the duotone layer or of the solid fill.
weightweightnumber2Stroke from 1 to 3 on the 24 grid, optically compensated below 24 px.
absoluteWeightabsolute-weightbooleanfalseKeeps the stroke at weight pixels at any size.
cornerscornersround, sharproundRound or sharp joins and caps.
variantvariantoutline, duotone, solidoutlineStyle axis; solid comes from the closed shapes.
rotaterotatenumber0Rotation in degrees.
flipXflip-xbooleanfalseMirror horizontally.
flipYflip-ybooleanfalseMirror vertically.
mirrorRtlmirror-rtlbooleanfalseMirror in right to left layouts.
animateanimatedraw, pop, wiggle, spin, pulse, bounce, shakenoneLiving state preset.
triggertriggermount, hover, click, statemountWhen the animation or the morph plays.
activeactivebooleanfalseThe state for trigger state.
durationdurationnumber (ms)per presetAnimation or morph duration.
delaydelaynumber (ms)0Animation or morph delay.
looploopboolean, numberfalseRepeat forever or a number of times (spin and pulse loop by default).
modifiermodifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotnoneBadge with an automatic knockout gap.
modifierCornermodifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightCorner of the badge.
morphTomorph-toname, datanoneMorph target; the trigger decides when it shows.
titletitlestringnoneAccessible name: title, role img and aria-label.
decorativedecorativebooleantrue without a titleHides the icon from assistive technology.

Icon adds name, icon (icon data) and gap. Fusion adds icons, layout, gap, scale and offsets. MorphIcon adds name and icon. A default slot renders inside the svg.

Pitfalls

An icon used by name is empty in production
The dev server pre-bundles the whole package, so every name resolves while you develop. A production build keeps only what you import, and Vite does not bundle the on demand loader that sits in node_modules: unregistered names in Icon, Fusion, MorphIcon or morph-to render an empty svg. Register them with registerIcons, as in src/icons.js above.
Icon only buttons without a name
Icons are decorative by default and get aria-hidden="true". A button that contains only an icon therefore has no accessible name. Put aria-label on the button, or give the icon a title. More patterns in the accessibility guide.
morph-to plays as soon as the page loads
The default trigger is mount: the icon morphs to its target right after mounting. When the target is the state you want to show immediately, use trigger="state" with :active, which starts at the right end without an animation.
Boolean props written as strings
active="false" passes the string "false". glyphfox reads it as false, but Vue warns about the prop type. Write boolean props bare (flip-x) or bind them (:active="playing").
Star imports
import * as icons from 'glyphfox/vue' followed by icons[name] keeps every icon in the bundle, because the bundler cannot know which names you read. Build a small map of the components you need instead.

Questions

Does glyphfox work with Vue 2?

The components need Vue 3.2 or newer. In a Vue 2 app, use the <glyph-fox> web component, which takes the same props as attributes; add glyph-fox to Vue.config.ignoredElements so Vue leaves the tag alone. See the web components guide.

Can I use the components with the Options API?

Yes. They are regular components: list them in components: { HomeIcon } and use them in the template. useMorph is a composable, so it belongs in setup().

Can my own SVG icons get the same props?

Yes. createIcon({ name, body, tags, categories }) from glyphfox/vue turns icon data into a component, where body is the inner markup of a 24 by 24 stroke icon. Run your SVG through Fox-ify in the studio first, so it follows the grid, stroke and padding rules that weight and the other axes rely on.

Icons in this guide