Frameworks

Icons in Lit and web components

glyphfox ships a custom element, <glyph-fox>, that takes the icon name and every shared prop as attributes. It works anywhere HTML works and fits Lit templates without a wrapper.

Install

Install the package and register the element once, in the entry module of your app. Importing glyphfox/web registers every icon and defines <glyph-fox> as a side effect, so nothing else is needed.

terminal
npm install glyphfox
main.js
import 'glyphfox/web';

To ship only the icons you use, take the element from glyphfox/web/element, register icon data from glyphfox/icons/NAME and define the element yourself. registerIcons comes from glyphfox/core, which shares its registry with the element.

icons.js
import { defineGlyphFox } from 'glyphfox/web/element';
import { registerIcons } from 'glyphfox/core';
import menu from 'glyphfox/icons/menu';
import x from 'glyphfox/icons/x';
import bell from 'glyphfox/icons/bell';
import search from 'glyphfox/icons/search';

registerIcons([menu, x, bell, search]);
defineGlyphFox();
Entry pointWhat it shipsSize (esbuild, minified)
glyphfox/webThe element, the engine and every icon, registered on import.about 209 KB, 59 KB gzip
glyphfox/web/elementThe element and the engine; you register the icons.about 77 KB, 28 KB gzip with one icon, plus a few hundred bytes per icon

A name you did not register is loaded on demand. webpack and esbuild turn those loads into one small chunk per icon, but a Vite production build leaves the import inside node_modules as written and the icon stays empty (see the tree shaking guide). Register every icon you use with glyphfox/web/element; they then also draw on the first frame, without a network round trip.

Use the element

The element renders an inline SVG into its shadow root. name takes the full name syntax, so modifier badges, fusions and styles need no extra API, and every shared prop is an attribute in kebab-case.

index.html
<glyph-fox name="home"></glyph-fox>
<glyph-fox name="file+check" size="32" weight="1.5"></glyph-fox>
<glyph-fox name="cloud&lock" layout="overlap"></glyph-fox>
<glyph-fox name="heart/solid" color="#e11d48"></glyph-fox>
<glyph-fox name="bell" animate="shake" trigger="hover"></glyph-fox>

The element observes its attributes. Change one with setAttribute, or through any template that writes attributes, and the icon redraws with the new value.

Lit templates

In Lit, bind values with attribute bindings. The element reads attributes and has no matching JavaScript properties, so a property binding such as .name=${...} sets a field the element never reads and the icon stays as it was.

app-toolbar.js
import { LitElement, html, css } from 'lit';
import 'glyphfox/web';

export class AppToolbar extends LitElement {
  static properties = {
    open: { type: Boolean },
    unread: { type: Number },
  };

  static styles = css`
    button { display: inline-flex; align-items: center; gap: 8px; color: inherit; }
    glyph-fox { color: var(--toolbar-icon, currentColor); }
  `;

  constructor() {
    super();
    this.open = false;
    this.unread = 0;
  }

  render() {
    return html`
      <button type="button" aria-expanded=${this.open} @click=${() => (this.open = !this.open)}>
        <glyph-fox name=${this.open ? 'x' : 'menu'} morph duration="300"></glyph-fox>
        <span>Menu</span>
      </button>
      <glyph-fox
        name="bell"
        animate="shake"
        trigger="state"
        ?active=${this.unread > 0}
        title=${this.unread ? `${this.unread} new notifications` : 'No new notifications'}
      ></glyph-fox>
    `;
  }
}

customElements.define('app-toolbar', AppToolbar);

Use boolean bindings for flags: ?morph=${...} and ?active=${...}. The element reads an empty attribute or "true" as true, and "false" or a missing attribute as false, so a boolean binding maps one to one.

For optional values, bind nothing (or use the ifDefined directive) so the attribute is removed when there is no value. An empty size attribute gives the inner svg an empty width and height, and the browser falls back to its default svg size.

icon-button.js
import { html, nothing } from 'lit';

export const iconButton = ({ name, size, label, onClick }) => html`
  <button type="button" aria-label=${label} @click=${onClick}>
    <glyph-fox name=${name} size=${size ?? nothing}></glyph-fox>
  </button>
`;

Styling through the shadow root

The svg lives in the element shadow root with its strokes in currentColor. Color inherits across the shadow boundary, so set color on the element or on any ancestor. The host is display: inline-flex with line-height: 0 and vertical-align: middle, so it sits on a line of text without extra CSS.

Size comes from the size attribute, which sets the width and height of the inner svg. CSS width and height on the host do not scale the drawing. Use size="1em" to follow the font size, or write the attribute from your own design tokens.

note.html
<p class="note">
  <glyph-fox name="circle-info" size="1em" weight="2.5"></glyph-fox>
  The icon follows the font size of this paragraph.
</p>
<glyph-fox name="folder" variant="duotone" color="#0f172a" secondary-color="#ff7a1a"></glyph-fox>
toolbar.css
.toolbar glyph-fox { color: #6b7280; }
.toolbar button:hover glyph-fox { color: #111827; }

There are no parts or slots to style: everything inside is generated from the props. Change the drawing with attributes such as weight, corners, variant and secondary-color, not with selectors that try to reach into the shadow root.

Your own tag name

defineGlyphFox(tag) defines the element under another name and returns its class. Use it to match the prefix of your design system or to avoid a clash with another element. It is safe to call more than once: a tag that is already defined is returned as it is.

icons.js
import { defineGlyphFox } from 'glyphfox/web/element';
import { registerIcons } from 'glyphfox/core';
import home from 'glyphfox/icons/home';
import settings from 'glyphfox/icons/settings';

registerIcons([home, settings]);
defineGlyphFox('acme-icon');
index.html
<acme-icon name="home" size="20"></acme-icon>
<acme-icon name="settings" size="20" weight="1.5"></acme-icon>

Importing glyphfox/web also defines glyph-fox, so pick one entry point per app: glyphfox/web for the default tag with every icon, glyphfox/web/element when you choose the tag and the icon list.

Triggers and morphs

The element handles its own interaction. With trigger="hover" or trigger="click" it listens on itself, and with trigger="state" it follows the active attribute. The morph attribute animates every change of name, while morph-to morphs toward a second icon whenever the trigger fires.

index.html
<glyph-fox name="menu" morph-to="x" trigger="click"></glyph-fox>
<glyph-fox name="sparkles" animate="draw" trigger="hover"></glyph-fox>
<glyph-fox id="visibility" name="eye" morph></glyph-fox>

<script type="module">
  const eye = document.getElementById('visibility');
  eye.addEventListener('click', () => {
    eye.setAttribute('name', eye.getAttribute('name') === 'eye' ? 'eye-off' : 'eye');
  });
</script>

Hover and click are measured on the element itself. The animation styles live inside the shadow root, where they cannot see the parent, and the click listener sits on the host. When a whole button should react, drive the icon from the button: use trigger="state" and set active from the button handlers.

save-button.html
<button type="button" class="save">
  <glyph-fox name="save" animate="pop" trigger="state"></glyph-fox>
  Save
</button>

<script type="module">
  const button = document.querySelector('.save');
  const icon = button.querySelector('glyph-fox');
  button.addEventListener('pointerenter', () => icon.setAttribute('active', ''));
  button.addEventListener('pointerleave', () => icon.removeAttribute('active'));
</script>

Morphs and presets respect prefers-reduced-motion: transitions jump to the end and animations do not play. The presets and triggers are described in animated icons.

Static SVG inside Lit templates

When an icon never changes after render, you can skip the element and insert the SVG markup. renderToString from glyphfox/core renders icon data with the shared props, and Lit inserts the string with the unsafeHTML directive. The renderer escapes titles and attribute values, so the markup is safe to insert.

saved-badge.js
import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { renderToString } from 'glyphfox/core';
import circleCheck from 'glyphfox/icons/circle-check';

const check = unsafeHTML(renderToString(circleCheck, { size: 16, color: '#16a34a' }));

export const savedBadge = () => html`<span class="badge">${check} Saved</span>`;

For the name syntax (file+check@tr, cloud&lock), use toSvg from glyphfox. It resolves any name, which means it carries every icon; see tree shaking before you use it in a client bundle.

Attributes

Every shared prop is an attribute in kebab-case. The element adds four of its own: name for the icon, with the full name syntax; layout and gap for fusions; and morph, which animates changes of name. Numbers are read from their text, and booleans follow the rule above.

AttributeTypeDefaultDescription
namestringnoneThe icon spec: a name, name+modifier@corner, a&b:layout or name/variant.
layoutoverlap, side, stack, grid, orbit, badge, insidenoneFusion layout when the name joins icons with &.
gapnumber1Knockout gap of a fusion, in grid units.
morphbooleanfalseAnimates every change of name as a morph.
sizenumber, string24Width and height; numbers are pixels.
colorstringcurrentColorMain color.
secondary-colorstringcolorColor of the duotone layer and of the solid fill.
secondary-opacitynumber0.3 / 1Opacity of the duotone layer or of the solid fill.
weightnumber2Stroke from 1 to 3 on the 24 grid, optically compensated below 24 px.
absolute-weightbooleanfalseKeeps the stroke at weight pixels at any size.
cornersround, sharproundRound or sharp joins and caps.
variantoutline, duotone, solidoutlineStyle axis; solid comes from the closed shapes.
rotatenumber0Rotation in degrees.
flip-xbooleanfalseMirror horizontally.
flip-ybooleanfalseMirror vertically.
mirror-rtlbooleanfalseMirror in right to left layouts.
animatedraw, pop, wiggle, spin, pulse, bounce, shakenoneLiving state preset.
triggermount, hover, click, statemountWhen the animation or the morph plays.
activebooleanfalseThe state for trigger state.
durationnumber (ms)per presetAnimation or morph duration.
delaynumber (ms)0Animation or morph delay.
loopboolean, numberfalseRepeat forever or a number of times (spin and pulse loop by default).
modifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotnoneBadge with an automatic knockout gap.
modifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightCorner of the badge.
morph-toname, datanoneMorph target; the trigger decides when it shows.
titlestringnoneAccessible name: title, role img and aria-label.
decorativebooleantrue without a titleHides the icon from assistive technology.

Accessibility

Without a label the icon is decorative: the inner svg gets aria-hidden="true". That is right for an icon next to visible text, and for an icon button that carries its own aria-label.

To give the icon itself a name, use the title attribute. It adds a <title>, role="img" and aria-label to the inner svg. Do not rely on aria-label on the element: the label stays on the host, which has no role, while the svg inside becomes an image without a name. title on the host is also the global HTML attribute, so browsers show it as a tooltip.

index.html
<!-- decorative: the button carries the name -->
<button type="button" aria-label="Close dialog">
  <glyph-fox name="x"></glyph-fox>
</button>

<!-- meaningful on its own -->
<glyph-fox name="triangle-alert" title="Warning" color="#b45309"></glyph-fox>

Patterns for links, status icons and contrast are in the accessibility guide.

Server rendering and layout shift

The element draws when it upgrades in the browser. Server rendered HTML holds only the empty tag, and the element does not use declarative shadow DOM, so the icon appears after the script runs. Reserve the space so the layout does not move when it does.

icons.css
glyph-fox:not(:defined) {
  display: inline-block;
  width: 24px;
  height: 24px;
  vertical-align: middle;
}
glyph-fox[size="16"]:not(:defined) {
  width: 16px;
  height: 16px;
}

When an icon has to be in the first paint, in email or in HTML for crawlers, render the SVG string on the server instead. The options for each framework are in server rendering.

Pitfalls

Self closing tags
Write <glyph-fox name="home"></glyph-fox>. HTML ignores the slash in <glyph-fox />, so the element stays open and the content after it becomes its children, which the shadow root does not show.
Property bindings
Lit .name=${...} and other property bindings set a JavaScript field that the element does not read. Bind attributes. Solid needs attr: for the same reason, see the Solid guide.
Empty attributes
An empty size renders an svg with empty width and height. Remove the attribute when there is no value, with nothing or ifDefined in Lit.
Hover and click on the parent
Hover and click triggers fire on the icon only, not on the button around it. Use trigger="state" and set active from the parent.
Attribute changes replay mount animations
The element swaps its inner svg whenever the markup changes, so with an animate preset and the default trigger="mount", changing color or size plays the animation again. Use trigger="state" for icons whose other attributes change often.
An empty icon
An unknown name renders nothing and logs a glyphfox: warning in the console. A valid name that is not registered draws as soon as its module loads, so check the network panel before you look for a typo.

Questions

Can I use the element in React, Vue or Angular?

Yes, it is a standard custom element. The framework packages are the better fit there, though: glyphfox/react, glyphfox/vue and glyphfox-angular give typed props and render on the server. Keep the element for Lit, plain HTML and pages that mix stacks. In Vue templates, tell the compiler that glyph-fox is a custom element with compilerOptions.isCustomElement, otherwise Vue tries to resolve it as a component.

Icons in this guide