Frameworks

Icons in Qwik

Most icons never change after the page loads. In Qwik they can be plain SVG written on the server, with no component code sent to the browser. For the few that morph or react, the <glyph-fox> element handles the interaction by itself.

Install

Install the core package. The server side approach uses glyphfox/core and one module per icon from glyphfox/icons/NAME; the element comes from glyphfox/web. Nothing needs to be configured in Vite or in Qwik.

terminal
npm install glyphfox

The examples import from @builder.io/qwik. Qwik 2 moves these imports to @qwik.dev/core; the glyphfox side stays the same.

Icons rendered on the server

renderToString returns the SVG markup for icon data and the shared props. Qwik inserts raw markup with dangerouslySetInnerHTML, which in Qwik takes the string itself, not an object as in React.

src/components/icon/icon.tsx
import type { IconData, SvgOptions } from 'glyphfox';
import { renderToString } from 'glyphfox/core';

type Props = SvgOptions & {
  icon: IconData;
  class?: string;
};

export const Icon = ({ icon, class: className, ...options }: Props) => (
  <span
    class={className}
    style="display:inline-flex;line-height:0"
    dangerouslySetInnerHTML={renderToString(icon, options)}
  />
);

Icon is a plain function, an inline component in Qwik terms. It renders as part of its parent and has no lazy loading boundary of its own. On the server the parent writes it into the HTML, and as long as the parent does not render again in the browser, no code for it is ever downloaded.

src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
import home from 'glyphfox/icons/home';
import settings from 'glyphfox/icons/settings';
import bookOpen from 'glyphfox/icons/book-open';
import { Icon } from '~/components/icon/icon';

export default component$(() => (
  <div class="shell">
    <nav class="sidebar">
      <a href="/"><Icon icon={home} size={20} /> Home</a>
      <a href="/docs/"><Icon icon={bookOpen} size={20} /> Docs</a>
      <a href="/settings/"><Icon icon={settings} size={20} weight={1.5} /> Settings</a>
    </nav>
    <main>
      <Slot />
    </main>
  </div>
));

If a component renders again on the client, for example because it reads a signal that changes, Qwik downloads its render function and the modules it imports, including the renderer from glyphfox/core. Keep icons in components that render once, and move the parts that change into their own components.

Motion without JavaScript

The markup from renderToString carries its own CSS. With animate it plays a preset, and with morphTo and trigger: 'hover' it morphs while the pointer is over the svg or its direct parent. Neither needs a script, and both stop under prefers-reduced-motion.

src/components/status.tsx
import arrowRight from 'glyphfox/icons/arrow-right';
import chevronRight from 'glyphfox/icons/chevron-right';
import loader from 'glyphfox/icons/loader';
import { Icon } from '~/components/icon/icon';

export const Next = () => (
  <a href="/next/" class="next">
    Next <Icon icon={arrowRight} morphTo={chevronRight} trigger="hover" />
  </a>
);

export const Saving = () => <Icon icon={loader} animate="spin" title="Saving" />;

With the core, morphTo takes icon data, not a name: import the target icon as above. A string is ignored and the icon stays still.

For a state such as open or selected, render with trigger: 'state': the svg then morphs when it gets the class gf-on. Toggling a class needs no re-render, so the only code the browser downloads is the click handler, and only on the first click.

src/components/menu-toggle.tsx
import { component$ } from '@builder.io/qwik';
import menu from 'glyphfox/icons/menu';
import x from 'glyphfox/icons/x';
import { Icon } from '~/components/icon/icon';

export const MenuToggle = component$(() => (
  <button
    type="button"
    aria-label="Menu"
    aria-expanded="false"
    onClick$={(_, button) => {
      const open = button.getAttribute('aria-expanded') !== 'true';
      button.setAttribute('aria-expanded', String(open));
      button.querySelector('svg')?.classList.toggle('gf-on', open);
    }}
  >
    <Icon icon={menu} morphTo={x} trigger="state" />
  </button>
));

Interactive icons with the element

When an icon has to switch between names, morph on every change, or react to clicks on its own, use the <glyph-fox> element. Load it from a visible task in the component that uses it, so the browser fetches it only when such an icon is on screen.

src/components/like-button.tsx
import { component$, useVisibleTask$ } from '@builder.io/qwik';

export const LikeButton = component$(() => {
  // eslint-disable-next-line qwik/no-use-visible-task
  useVisibleTask$(() => {
    import('glyphfox/web');
  });

  return (
    <button type="button" class="like" aria-label="Like">
      <glyph-fox name="heart" morph-to="heart/solid" trigger="click"></glyph-fox>
    </button>
  );
});

Write the props of the element as string attributes, as in HTML: name, size, morph, morph-to, trigger. With a click or hover trigger the element keeps its own state, so the component never renders again and Qwik has nothing to resume for it.

glyphfox/web carries every icon. To load less, import a module of your own from the task that registers a subset with defineGlyphFox and registerIcons, as in the web components guide. If your TypeScript setup rejects the unknown tag, declare glyph-fox among the JSX intrinsic elements in a declaration file.

The element draws after it upgrades, so the server HTML holds an empty tag. Reserve its size with CSS so the layout does not move:

src/global.css
glyph-fox:not(:defined) {
  display: inline-block;
  width: 24px;
  height: 24px;
}

Qwik City

Layouts and routes render on the server, so the Icon component works in src/routes/layout.tsx, in pages and in every component they render. Static site generation writes the same markup into HTML files.

When icon names come from data, resolve them on the server. In a routeLoader$, toSvg from glyphfox accepts any name and the full name syntax; it carries every icon, which is fine in server only code.

src/routes/features/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { toSvg } from 'glyphfox';

const FEATURES = [
  { icon: 'shield-check', label: 'Audited' },
  { icon: 'cloud&lock:overlap', label: 'Encrypted backups' },
  { icon: 'file+check@tr', label: 'Signed releases' },
];

export const useFeatures = routeLoader$(() =>
  FEATURES.map((f) => ({ ...f, svg: toSvg(f.icon, { size: 28, weight: 1.75 }) })),
);

export default component$(() => {
  const features = useFeatures();
  return (
    <ul class="features">
      {features.value.map((f) => (
        <li key={f.label}>
          <span dangerouslySetInnerHTML={f.svg} />
          {f.label}
        </li>
      ))}
    </ul>
  );
});

To have the element on every page instead, add the single file CDN build to the head in src/root.tsx with defer, as described in HTML and CDN. That trades a 62 KB gzip download on each page for not tracking where the element is used.

Props

The Icon component forwards its props to renderToString, which takes the shared options with their camelCase names, plus class and style strings for the svg. On the element the same props are attributes in kebab-case.

OptionTypeDefaultDescription
sizenumber, string24Width and height; numbers are pixels.
colorstringcurrentColorMain color.
secondaryColorstringcolorColor of the duotone layer and of the solid fill.
secondaryOpacitynumber0.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.
absoluteWeightbooleanfalseKeeps 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.
flipXbooleanfalseMirror horizontally.
flipYbooleanfalseMirror vertically.
mirrorRtlbooleanfalseMirror 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.
modifierCornertop-left, top-right, bottom-left, bottom-rightbottom-rightCorner of the badge.
morphToname, 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.

Pitfalls

dangerouslySetInnerHTML takes a string
Pass the markup itself. The React form with __html does not work in Qwik.
Parents that render again
An icon inside a component that renders again in the browser brings glyphfox/core with it, about 37 KB minified. Keep stateful parts in small components of their own.
Names in morphTo
The core needs icon data for morphTo. A string is ignored, and the icon does not morph.
Hover reaches only the direct parent
The CSS hover morph reacts to the svg and its direct parent, which is the wrapper span. Hovering a larger button around it does not start the morph; for that, toggle gf-on with trigger: 'state'.
toSvg in client code
import { toSvg } from 'glyphfox' carries every icon. It belongs in server only code such as routeLoader$; in components that may run in the browser, use the core with per icon modules.
The element before it upgrades
Server HTML holds an empty <glyph-fox>. Reserve its box with CSS, or render icons that must be visible at once with the Icon component.

Questions

Do server rendered icons really ship no JavaScript?

Yes, as long as the component that renders them does not render again in the browser. The HTML holds plain SVG, and presets and hover morphs are CSS inside that SVG. Only components that render again, and the <glyph-fox> element, bring code to the browser.

Can I use the React components through qwikify$?

They behave like any React component inside qwikify$, but that island then ships React and the glyphfox React runtime. For icons, server rendered markup or the element costs far less.

Icons in this guide