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>
AspectWeb fontSVG+JS
Load costOne font file per style usedOne JS bundle, larger
Runtime costNone after the font loadsA MutationObserver plus a replacement pass
Rendering controlFont metrics; layers need extra markupFull SVG: per-path fill, transforms, power transforms
Layout shiftYes: glyph width changes when the font loadsNo: the SVG is sized with the replacement
AccessibilityPseudo-element content, needs careReal <svg> with role and a title
AnimationsLimited to transform and opacityfa-beat, fa-fade, and any SVG animation
Works without JavaScriptYesNo
CSPFont-src onlyScript-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>.
SituationSettingWhy
A framework that owns the DOMComponents, not dom.watch()Avoids the watcher fighting the renderer
Markup added by a third-party widgetobserveMutationsRoot scopedOnly watch what can contain icons
A large page that does not changeobserveMutations = falseNo observer cost at all
Strict CSPautoAddCss = false plus your own stylesheetNo injected style element
Server renderingicon().htmlProduces the SVG string directly
An icon missing from the free setshowMissingIcons on in developmentTurns a silent blank into a console warning

Choosing between the two

You needWeb fontSVG+JS or the SVG component
Icons with no JavaScript at allThe only optionNo
Multi-colour duotone layersExtra markup per iconNative
Power transforms and stackingUtility classes with a wrapper elementNative SVG transforms
A per-icon animationTransform and opacity onlyAnything SVG can do
The smallest possible payloadSmall after subsettingLarger unless tree-shaken
A strict CSP with no inline stylesCleanNeeds configuration
Framework-rendered iconsAwkward: the class is the APIA 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.

Kits, CDN and self-hosting Font Awesome in React, Vue and Angular

Last refreshed 2026-09-18.