(written by Grok and reviewed by Max Milbers)

Audience: Template builders, anyone putting an icon in markup
File: media/com_virtuemart/js/icons/vm-icon-single-shim.module.js
Loaded: vmJsApi::addvScriptModule('icons/vm-icon-single-shim.module') from the init registry bootstrap (priority 40, right after Alpine at 30)

1. Why not a sprite, why not an icon font

VirtueMart 4 admin lived on UIKit icons: a fat JS blob or a sprite with every glyph, whether the page used three icons or eighty. Updating one glyph meant touching the blob, fighting linebreaks in SVG paths, and shipping kilobytes nobody fetched.

The single-shim does the opposite:

  • Each icon is a normal file: assets/images/icons/cart.svg.
  • The page only downloads the names it actually contains.
  • No closed icon font, no build step to pack a sprite.
  • Markup stays a <span>. The shim injects the SVG inside it.

That is the product claim: drop an SVG in a folder, write the name in HTML, done.

2. Why not just put the SVG URL in the HTML?

You can. These all work without a shim:

<img src="/…/assets/images/icons/cart.svg" alt="" width="20" height="20">
<svg><use href="/…/icons.svg#cart"></use></svg>

The loader is not required for the file to display. It is required for how VirtueMart wants to use the file.

What a raw URL does not give you

  • Styling from the page. An <img src="/cart.svg"> is a separate document. CSS color, currentColor, dark-mode fills, md-color-* on a parent do not paint the paths. The shim inlines the SVG into the span so the icon is real DOM and takes the text colour (status green/red, Atum dark, button hover).
  • The old markup. Hundreds of uk-icon="icon: cart; ratio: 1.2" already exist. A URL in every template would be a rewrite of admin + overrides. The shim keeps the name; it invents the URL.
  • One name, several bases. cart or a template-own my-brand — authors write the stem, not the path. On the site the loader tries the shop template, then Joomla media, then core (incl. a /joomla5/ subdir from import.meta.url).
  • AJAX / Alpine / Mustache. A Mustache card injects <span vm-icon="trash"> with no src. The observer fetches then. With <img src> every template string must know the full URL including the Joomla root.
  • Ratio. ratio: 1.2 is 24px without a utility class per size.

Load speed: first the page, then the icons?

Yes — that is a side effect, not magic faster than <img>.

  • HTML/CSS first. The document is not stuffed with 40 SVG payloads. Text, layout, Choices, Alpine run. Spans are empty until fetch returns. That feels like “the page is there, then the glyphs pop in.”
  • Only used names. Versus the old UIKit blob, you do not download eighty icons to show five. Versus <img> tags for those same five, bytes are similar.
  • <img src> can start earlier. The browser preload scanner sees URLs in HTML and starts GET while parsing. The shim waits for JS (prio 40) then fetches. Total time-to-icon can be worse than inline <img>. You trade a few hundred milliseconds of empty spans for styling, old markup, and no path in every override.
  • Cache. The second page with the same cart.svg hits disk/HTTP cache. The in-memory Map only helps inside one page (Mustache adding a second trash icon).

So: the loader is a compatibility + styling + naming layer. It is not faster than putting five <img> in the HTML. It is simpler for authors than those five URLs, and it is much lighter than a sprite of everything. If we ever drop uk-icon and do not need currentColor on paths, a plain <img> (or CSS mask) would be enough. We are not there; we still have UIKit attributes and coloured status icons.

3. What you write in HTML

Preferred (VirtueMart 5):

<span vm-icon="icon: cart; ratio: 1.2"></span>
<span vm-icon="trash"></span>

Still accepted (UIKit leftover during the BS5 migration):

<span uk-icon="icon: cart; ratio: 1.2"></span>

The parser splits on ;:

  • icon: name — file stem, no .svg. Or a bare token if there is no colon (vm-icon="plus").
  • ratio: 1.2 — size is 20 × ratio pixels (default ratio 1 → 20×20).

vm-icon wins if both attributes exist.

After a successful load the shim:

  1. Empties the span (keeps the span, so uk-icon-button / vm-icon-button classes stay).
  2. Appends a cloned <svg> with classes uk-icon vm-icon-single plus any md-color-* found on the host or parents (status colours in dark mode).
  3. Removes vm-icon / uk-icon so the MutationObserver does not loop.
  4. Sets data-vm-icon="cart" so you can still see which glyph it is.

If the file is missing, the attributes are stripped and the span stays empty. Check the console: [VMIconSingleShim] Failed to load individual icon ….

4. When things run (this is the dark bit)

The file is an ES module. Two clocks:

A. Module evaluation (as soon as the browser finishes importing it, possibly before DOMContentLoaded):

  • Installs a UIkit stub on window.UIkit: UIkit.use, UIkit.icon.add, UIkit.icon._registry. Old uikit-icons.js / vmuikit-icons.js can still UIkit.icon.add({ cart: '<svg…>' }) without a real UIKit. The modern path does not load those blobs.
  • Computes ICON_BASE and JOOMLA_ROOT from import.meta.url (see paths).
  • Starts a MutationObserver on documentElement so Alpine, Mustache, AJAX, and later innerHTML still get icons.
  • Exposes window.VMIconSingleShim and listens for vm:icons:refresh.
  • Registers with VMInit at priority 40.

B. VMInit callback (after Alpine at 30, before most core ≤500):

  • Runs processIcons(document): collect unique names, fetch them in parallel, then replace.

So: stub + observer are early. The first paint of icons waits for prio 40 plus the SVG fetches. That is why the shim is loaded in loadVmInitRegistry(), not per view.

5. Where the SVG is fetched (the other dark bit)

Core icons come from Virtuemart.imagesPath (PHP: media/com_virtuemart/images/) plus icons/. The Joomla subdirectory (for example /joomla5/) is taken from Virtuemart.vmSiteurl. The query string of the module URL is still appended to each SVG request (same ?vmver= as the JS, when present).

This is not a Joomla admin override of core files. It is how a shop template uses its own icons with the loader: drop my-brand.svg in the template folder, write <span vm-icon="my-brand">. Same stem as core (cart.svg) on the site wins; that is a side effect, not the point. Admin stays on core.

getCandidateBases() when Virtuemart.isSite is true and Virtuemart.siteTemplate is set:

  1. Site template: templates/yourtemplate/html/com_virtuemart/assets/images/icons/
  2. Joomla template media: media/templates/site/yourtemplate/html/com_virtuemart/assets/images/icons/
  3. Joomla HTMLHelper-style: media/templates/site/yourtemplate/images/com_virtuemart/icons/
  4. Core last: Virtuemart.imagesPath + icons/media/com_virtuemart/images/icons/

When isSite is false (admin, or a manager on the frontend), template folders are skipped. Component media still applies if the folder exists. No admin-template icon folder. A missing file at a template path is a normal 404; the loader continues. Debug with:

VMIconSingleShim.getBases()

404 on a template path is normal — the loader tries the next base. Fetch uses credentials: 'same-origin' and cache: 'no-cache'.

SVG is parsed as HTML, not XML. Firefox otherwise throws if the server returned an HTML error page instead of SVG. querySelector('svg') picks the node in both cases.

6. How to use SVG icons in your template

This is for shop templates: your own glyphs with the VirtueMart loader. It is not a Joomla admin override of core files.

  1. Take any SVG (the XML, not a font ligature).
  2. Save it as templates/{yourTemplate}/html/com_virtuemart/assets/images/icons/my-cool-icon.svg.
  3. Write <span vm-icon="my-cool-icon"></span> (or vm-icon="icon: my-cool-icon; ratio: 1.2").

Name = filename without .svg. If you also ship Cassiopeia-style template media, the same file may live under media/templates/site/yourtemplate/html/com_virtuemart/assets/images/icons/. Core icons are in media/com_virtuemart/images/icons/.

Joomla’s place for component CSS, JS and images is media/com_virtuemart/. That is not the same folder as media/templates/site/… (template assets). How setPath overlays a single file: Technics article on public assets in media/com_virtuemart.

7. AJAX, Alpine, Mustache

The observer watches the whole document for new [vm-icon] / [uk-icon]. You usually do nothing.

If you inject HTML and icons stay empty (observer missed a text-only swap, or you stripped attributes already), fire:

document.dispatchEvent(new Event('vm:icons:refresh'));
// or
VMIconSingleShim.process(document);

Mustache order-status popups and media cards use vm-icon in the template string. After Mustache renders into the DOM, the observer should pick them up. If not, refresh.

8. Public API

   
VMIconSingleShim.process(root) Scan and replace under root (default document)
VMIconSingleShim.load(name) Promise of cached SVG entry or null
VMIconSingleShim.cache Map name → { text, el } or null after a failed load
VMIconSingleShim.getBases() URL list the loader will try
Event vm:icons:refresh Full document rescan

A failed name is cached as null so the loader does not hammer 404s. Reload the page after you add a missing file.

9. Do’s and don’ts

  • Do keep a real element (span, a) as host. Do not put vm-icon on the <svg> you already inlined.
  • Do not load the old UIKit icon pack on the modern admin path — the stub is enough for leftover uk-icon.
  • Do not expect a sprite id (#icon-cart). The file is cart.svg.
  • Do not call process in a tight loop; the observer is already there.
  • Do copy md-color-* onto the host if the icon must keep a status colour in dark mode (the shim walks 5 parents).

10. Files

  • Loader: media/com_virtuemart/js/icons/vm-icon-single-shim.module.js
  • Core SVGs: media/com_virtuemart/images/icons/*.svg
  • Template icons: templates/yourtemplate/html/com_virtuemart/assets/images/icons/
  • Joomla media template: media/templates/site/yourtemplate/html/com_virtuemart/assets/images/icons/ and media/templates/site/yourtemplate/images/com_virtuemart/icons/
  • Bootstrap: vmJsApi::loadVmInitRegistry() / Virtuemart.imagesPath