(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. CSScolor,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.
cartor a template-ownmy-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 fromimport.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.2is 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.svghits disk/HTTP cache. The in-memoryMaponly 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 is20 × ratiopixels (default ratio 1 → 20×20).
vm-icon wins if both attributes exist.
After a successful load the shim:
- Empties the span (keeps the span, so
uk-icon-button/vm-icon-buttonclasses stay). - Appends a cloned
<svg>with classesuk-icon vm-icon-singleplus anymd-color-*found on the host or parents (status colours in dark mode). - Removes
vm-icon/uk-iconso the MutationObserver does not loop. - 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. Olduikit-icons.js/vmuikit-icons.jscan stillUIkit.icon.add({ cart: '<svg…>' })without a real UIKit. The modern path does not load those blobs. - Computes
ICON_BASEandJOOMLA_ROOTfromimport.meta.url(see paths). - Starts a MutationObserver on
documentElementso Alpine, Mustache, AJAX, and laterinnerHTMLstill get icons. - Exposes
window.VMIconSingleShimand listens forvm: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,fetchthem 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:
- Site template:
templates/yourtemplate/html/com_virtuemart/assets/images/icons/ - Joomla template media:
media/templates/site/yourtemplate/html/com_virtuemart/assets/images/icons/ - Joomla HTMLHelper-style:
media/templates/site/yourtemplate/images/com_virtuemart/icons/ - 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.
- Take any SVG (the XML, not a font ligature).
- Save it as
templates/{yourTemplate}/html/com_virtuemart/assets/images/icons/my-cool-icon.svg. - Write
<span vm-icon="my-cool-icon"></span>(orvm-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 putvm-iconon 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 iscart.svg. - Do not call
processin 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/andmedia/templates/site/yourtemplate/images/com_virtuemart/icons/ - Bootstrap:
vmJsApi::loadVmInitRegistry()/Virtuemart.imagesPath