SVG with JavaScript: how it works and when to use it
Understand the automatic replacement pass, its MutationObserver cost, how to disable or scope it, and when web fonts are still the better choice.
What the replacement actually does
// Font Awesome's SVG+JS core ships as part of the free package.
import { library, dom, config } from '@fortawesome/fontawesome-svg-core';
import { faCartShopping, faBell } from '@fortawesome/free-solid-svg-icons';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
library.add(faCartShopping, faBell, faGithub);
// dom.watch() scans the document for <i class="fa-..."> elements and replaces
// each one with an inline <svg>. It then installs a MutationObserver so that
// markup added later is replaced too.
dom.watch();
// Before:
// <i class="fa-solid fa-cart-shopping" aria-hidden="true"></i>
// After:
// <svg class="svg-inline--fa fa-cart-shopping" aria-hidden="true"
// data-fa-i2svg="" viewBox="0 0 576 512" role="img">
// <path d="M0 24C0 10.7 ..."></path>
// </svg>| Aspect | Web font | SVG+JS |
|---|---|---|
| Load cost | One font file per style used | One JS bundle, larger |
| Runtime cost | None after the font loads | A MutationObserver plus a replacement pass |
| Rendering control | Font metrics; layers need extra markup | Full SVG: per-path fill, transforms, power transforms |
| Layout shift | Yes: glyph width changes when the font loads | No: the SVG is sized with the replacement |
| Accessibility | Pseudo-element content, needs care | Real <svg> with role and a title |
| Animations | Limited to transform and opacity | fa-beat, fa-fade, and any SVG animation |
| Works without JavaScript | Yes | No |
| CSP | Font-src only | Script-src plus inline styles |
💡
Replacement is one-way. The
<i> element is gone from the DOM, so anything that queried it, set a class on it, or attached a listener to it stops working. In a framework that re-renders nodes, the framework and the watcher can end up in a loop — which is why the official component packages exist.Controlling the replacement
import { dom, config, icon, library } from '@fortawesome/fontawesome-svg-core';
import { faBell } from '@fortawesome/free-solid-svg-icons';
// 1. Disable auto-replacement entirely and insert icons explicitly.
config.autoReplaceSvg = false;
config.observeMutations = false;
library.add(faBell);
// Insert an icon where you want it, as the last child of an element.
const bell = icon({ prefix: 'fas', iconName: 'bell' }, { classes: ['text-lg'] });
document.querySelector('.toolbar').appendChild(bell.node[0]);
// Get just the HTML string, if you are rendering on the server.
const { html } = icon({ prefix: 'fas', iconName: 'bell' }, { title: 'Notifications' });
// 2. Keep auto-replacement but stop observing the DOM: cheaper, and markup
// added later must be replaced by an explicit call.
config.autoReplaceSvg = true;
config.observeMutations = false;
dom.watch();
function replaceIconsIn(root = document.body) {
dom.i2svg({ node: root });
}
// 3. Scope the observer to a container instead of the whole document.
config.mutateApproach = 'sync'; // 'async' (default) or 'sync'
// and observe a narrower root:
dom.watch({ observeMutationsRoot: document.querySelector('#app') });// Configuration options worth knowing
config.set({
familyPrefix: 'fa', // 'fa' for v6/7, 'fa5' style prefixes for older
replacementClass: 'svg-inline--fa',
autoReplaceSvg: 'nest', // 'nest' inserts inside, 'replace' swaps the element
keepOriginalSource: false, // true leaves a comment with the original markup
showMissingIcons: true, // log a warning for an icon not in the library
autoAddCss: true // inject the CSS the SVG needs
});
// Disabling autoAddCss means you must ship the core stylesheet yourself, which
// is the route to take if a strict CSP forbids injected inline styles:
// config.autoAddCss = false;
// then link the stylesheet in your <head>.| Situation | Setting | Why |
|---|---|---|
| A framework that owns the DOM | Components, not dom.watch() | Avoids the watcher fighting the renderer |
| Markup added by a third-party widget | observeMutationsRoot scoped | Only watch what can contain icons |
| A large page that does not change | observeMutations = false | No observer cost at all |
| Strict CSP | autoAddCss = false plus your own stylesheet | No injected style element |
| Server rendering | icon().html | Produces the SVG string directly |
| An icon missing from the free set | showMissingIcons on in development | Turns a silent blank into a console warning |
Choosing between the two
| You need | Web font | SVG+JS or the SVG component |
|---|---|---|
| Icons with no JavaScript at all | The only option | No |
| Multi-colour duotone layers | Extra markup per icon | Native |
| Power transforms and stacking | Utility classes with a wrapper element | Native SVG transforms |
| A per-icon animation | Transform and opacity only | Anything SVG can do |
| The smallest possible payload | Small after subsetting | Larger unless tree-shaken |
| A strict CSP with no inline styles | Clean | Needs configuration |
| Framework-rendered icons | Awkward: the class is the API | A component is the API |
<!-- The decision in practice: a content site with 40 icons used site-wide
is well served by a subset web font. An application with 300 icons,
duotone states and animated feedback is not. -->
<!-- A duotone icon needs the two layers to be independently coloured, which
is a single element with the SVG route and a wrapper with the font route. -->
<!-- Font route: two stacked pseudo-elements -->
<span class="fa-stack fa-2x">
<i class="fa-solid fa-circle fa-stack-2x" style="color:#c4b5fd"></i>
<i class="fa-solid fa-bolt fa-stack-1x" style="color:#4c1d95"></i>
</span>
<!-- SVG route: one element, two paths, two CSS custom properties -->
<i class="fa-duotone fa-bolt icon"
style="--fa-primary-color:#4c1d95; --fa-secondary-color:#c4b5fd"></i>The pragmatic recommendation for a new project: start with the framework component packages and SVG, because they tree-shake, they behave predictably inside a rendering loop, and they give you the full styling toolkit. Fall back to a subset web font only when you genuinely cannot ship JavaScript — a static site, an email template, or a page that must render icons with scripting disabled.
FAQ
Why do my icons disappear in a single-page app?
The router replaced the DOM after
dom.watch() had already run, or the framework re-rendered and the watcher's replacement was undone. Use the framework's own Font Awesome component, or call dom.i2svg() on the new subtree after each navigation.Does SVG+JS hurt performance?
The MutationObserver is the cost, not the replacement itself. On a page that renders hundreds of icons at once the watcher fires per mutation, so batching the DOM insertion — or turning the observer off and calling
i2svg once — makes a measurable difference.Related
Kits, CDN and self-hosting Font Awesome in React, Vue and Angular
Last refreshed 2026-09-18.