- 🔒 Shadow DOM isolation — SVG styles and IDs are scoped to the component. No conflicts with the rest of the page.
- 📦 Smart caching — LRU in-memory cache with deduplication. Same URL fetched once, shared across all instances. Configurable by item count and byte size limit.
- 🖼️ srcset support — serve different SVG files based on the component's rendered width, just like native
<img srcset>. - ⚡ Loading strategies —
eager,defer,idle, andlazy(viaIntersectionObserver). - 🔗 Base URL — resolve src against a configurable base path or CDN URL. Set per-element or globally via defaults.
- 🎨 Flexible styling — inject CSS into the shadow DOM globally via
define()or per-instance viacomponentStyles. - 🧹 Optional sanitization — plug in any sanitizer (e.g. DOMPurify) to clean SVG nodes before rendering.
- 📐 Responsive — automatic candidate swapping on resize via
ResizeObserver. - 🧩 Extensible — designed to be subclassed. Override fetching, sanitization, rendering, or defaults.
- Installation
- Usage
- Custom definition
- Instance styles
- Default styles
widthandheight- Base URL
- Cache
- Loading Strategies
- srcset & Responsive
- Sanitize
- Styling the inner SVG
- Attributes
- Events
npm install @components-1812/svg-isolateLoads the bundle and registers <svg-isolate> automatically with default styles included.
<script type="module">
import "https://cdn.jsdelivr.net/npm/@components-1812/svg-isolate/dist/index.bundle.min.js";
</script>Use this if you need a custom tag name or want to provide your own styles.
<script type="module">
import SVGIsolate from "https://cdn.jsdelivr.net/npm/@components-1812/svg-isolate/dist/SVGIsolate.min.js";
SVGIsolate.define("custom-svg-isolate", {
links: [
"https://cdn.jsdelivr.net/npm/@components-1812/svg-isolate/dist/SVGIsolate.min.css",
],
});
</script>| File | jsdelivr | unpkg |
|---|---|---|
| Bundle (recommended) | link | link |
| SVGIsolate.js | link | link |
| SVGIsolate.css | link | link |
Import the component in a client-side script file:
import "@components-1812/svg-isolate";This loads the bundle, auto-defines the custom element as
<svg-isolate>, and applies the default styles viaadoptedStyleSheets.
<!-- inline SVG -->
<svg-isolate>
<svg width="200" height="200"><!-- SVG content --></svg>
</svg-isolate>
<!-- load from file -->
<svg-isolate src="path/to/circle.svg" />
<svg-isolate src="path/to/hexagon.svg" loading="lazy" />
<svg-isolate srcset="icon-300.svg 300w, icon-600.svg 600w" />If you need to register the element under a different tag name or inject custom styles into its shadow DOM, use SVGIsolate.define() directly instead of the auto-import.
Best for programmatically constructed styles or when working with a build system that produces CSSStyleSheet objects.
import SVGIsolate from "@components-1812/svg-isolate/SVGIsolate.js";
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: inline-block; }`);
SVGIsolate.define("custom-svg-isolate", { adopted: [sheet] });Best for inlining styles directly without an external file.
import SVGIsolate from "@components-1812/svg-isolate/SVGIsolate.js";
SVGIsolate.define("custom-svg-isolate", {
raw: [`:host { display: inline-block; }`],
});Best for loading styles from a CSS file at runtime. URLs are resolved against document.baseURI, so relative paths are accepted.
import SVGIsolate from "@components-1812/svg-isolate/SVGIsolate.js";
SVGIsolate.define(null, { links: ["/path/to/styles.css"] });All three options can be combined in a single define() call:
SVGIsolate.define("custom-svg-isolate", {
adopted: [sheet],
raw: [":host { display: block; }"],
links: ["/path/to/styles.css"],
});Duplicate entries are ignored automatically — adding the same URL or CSSStyleSheet object twice has no effect.
Every <svg-isolate> element exposes a componentStyles property — a ComponentStyles instance that controls the styles injected into its shadow DOM. You can add or replace styles on a specific element at any time without affecting other instances.
componentStyles.add() accepts the same { links, adopted, raw } shape as define(). Chain .apply() to re-render the shadow DOM styles immediately.
const el = document.querySelector("svg-isolate");
el.componentStyles
.add({ raw: [`:host { outline: 2px solid red; }`] })
.apply();Duplicate entries are ignored — adding the same URL or raw string twice has no effect.
CSS injected this way lives inside the shadow root, so it can reach the SVG elements directly:
document.querySelector('svg-isolate[sanitize]')
.componentStyles
.add({
raw: `
circle { fill: #5f000d; }
rect { fill: #070070; }
text { fill: #c5b800; font-family: serif; }
`,
})
.apply();Each style type is a StyleCollection instance and can be manipulated directly before calling .apply():
const { raw, links, adopted } = el.componentStyles;
// check what's already registered
console.log(raw.size); // number of raw CSS strings
console.log(links.size); // number of external stylesheets
// check if a specific entry exists
links.has("https://example.com/theme.css");
// iterate over current entries
for (const url of links) {
console.log(url);
}
// remove everything from one collection and replace it
raw.clear();
raw.add([`circle { fill: hotpink; }`]);
el.componentStyles.apply();el.componentStyles.add({ links: ["/themes/dark.css"] }).apply();The ready-links event fires once the stylesheet has loaded.
Call .clear() before .add() to discard everything and start fresh:
el.componentStyles
.clear()
.add({ raw: [`:host { background: #000; }`] })
.apply();Note:
componentStylesis per-instance. Changes made to one element do not affect other<svg-isolate>elements on the page, even if they share the samesrc.
When loaded via the auto-import bundle, ships with these default host styles:
:host {
position: relative;
display: inline-block;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
contain: size;
overflow: hidden;
}
:host svg {
display: block;
width: 100%;
height: 100%;
}From: /src/SVGIsolate.css
The most important of these is contain: size — it prevents the component from triggering
layout recalculations in its parent (particularly relevant inside flex and grid containers,
where an unsized inline element can cause repeated reflows).
If you register the component manually via SVGIsolate.define(), none of these styles are applied automatically.
You can inject them (or a modified version) via the adopted, raw, or links options — see Custom definition.
Note that width: 100%; height: 100% means the component sizes itself to its container —
if the container has no explicit dimensions, the component collapses to zero.
Use the width and height attributes or size the container from CSS.
To override the defaults on a specific instance without touching others, use componentStyles directly:
el.componentStyles
.clear()
.add({ raw: [`:host { display: block; width: 300px; height: 300px; }`] })
.apply();See Instance styles for the full API.
Sets style.width and style.height on the <svg-isolate> host element directly.
<svg-isolate src="icon.svg" width="200px" height="200px" />
<svg-isolate src="banner.svg" width="100%" height="4rem" />el.width = "50%";
el.height = "120px";Accepts any valid CSS length value. Equivalent to setting style.width / style.height inline — useful when you want to control dimensions declaratively via HTML rather than in your stylesheet.
You can provide a base attribute to resolve the src URL against a specific base path rather than the document's base URI.
The base is always a fixed URL to which the src is concatenated. If base is relative, it is resolved against document.baseURI. Then, if src is also relative, its resolved path is appended to the base.
The resulting URL follows this structure:
<base origin>/<base path>/<src path>?<src query>#<src hash>- The default value of
baseis"/". - If
srcis an absolute URL,baseis ignored entirely andsrcis used as-is. - The resolution logic is handled internally by the static method
SVGIsolate.resolveSource(src, base)
<svg-isolate src="/icons/circle.svg" base="/assets"></svg-isolate>
<svg-isolate src="circle.svg" base="https://cdn.example.com"></svg-isolate>el.base = "/assets";To apply a fixed base to all instances of a custom element, set it in SVGIsolate.defaults.base before calling define():
class BootstrapIcon extends SVGIsolate {
static defaults = {
...super.defaults,
base: "https://raw.githubusercontent.com/twbs/icons/refs/heads/main/icons",
};
}
BootstrapIcon.define("bootstrap-icon", {
links: [
"https://cdn.jsdelivr.net/npm/@components-1812/svg-isolate@0.0.2/dist/SVGIsolate.min.css",
],
});Now every <bootstrap-icon> resolves src against that CDN path without needing base on each element:
<bootstrap-icon src="circle.svg"></bootstrap-icon>
<!-- → https://raw.githubusercontent.com/twbs/icons/refs/heads/main/icons/circle.svg -->Here are a few representative examples of how different inputs are resolved (assuming a document URI of http://127.0.0.1:3000/docs/examples/base-test/):
src |
base |
Resolved URL | Description |
|---|---|---|---|
https://raw.example.com/circle.svg |
/docs |
https://raw.example.com/circle.svg |
Absolute URL src, base is ignored |
/assets/circle.svg |
/docs |
http://127.0.0.1:3000/docs/assets/circle.svg |
Root-relative src treated relative to base path |
../../../assets/circle.svg |
/docs |
http://127.0.0.1:3000/docs/assets/circle.svg |
Root-relative nested src with path base |
/0-circle.svg |
https://raw.example.com |
https://raw.example.com/0-circle.svg |
Root-relative src with absolute domain base |
circle.svg |
/ |
http://127.0.0.1:3000/docs/examples/base-test/circle.svg |
Relative src with default base |
assets/circle?w=150#svg |
/docs |
http://127.0.0.1:3000/docs/docs/examples/base-test/assets/circle?w=150#svg |
With query params and hash |
By default, <svg-isolate> caches every SVG source in memory after the first fetch, so subsequent requests for the same URL are served instantly without hitting the network.
Use the no-cache attribute:
<svg-isolate src="path/to/file.svg" no-cache />Or via the .useCache property:
const svg = document.querySelector("svg-isolate");
svg.useCache = false;To disable caching for all instances:
SVGIsolate.defaults.useCache = false;To disable the cache system completely — no cache is created at define() time, .useCache always returns false and cannot be set to true:
SVGIsolate.CACHE_ENABLED = false;Must be set before calling SVGIsolate.define().
By default, the cache holds up to 100 entries with no maximum cumulative byte size limit (Infinity). When limits are reached, the least recently used entry is evicted before adding the new one (LRU):
// Evict after 50 items
SVGIsolate.CACHE_MAX_ENTRIES = 50;
// Evict after 10 Megabytes of accumulated SVG strings
SVGIsolate.CACHE_MAX_SIZE = '10mb'; // Also accepts '500kb', '1.5g', or raw bytes like 5000000Both must be set before calling SVGIsolate.define().
Note
The size values are parsed case-insensitively (e.g., '1mb' is identical to '1MB') and represent sizes in bytes rather than bits (e.g., '1MB' or '1mb' is parsed as exactly
The cache is shared across all instances of the same component class. Two <svg-isolate> elements pointing to the same src will only trigger one fetch — the second reuses the cached result.
SVGIsolate.CACHE.clear(); // clear all entries
SVGIsolate.CACHE.delete(src); // remove a specific entry
SVGIsolate.CACHE.has(src); // check if a src is cached
SVGIsolate.CACHE.values; // Map with all cached entriesYou can manually populate the cache before any component renders:
await SVGIsolate.CACHE.fetchSVG("/assets/icon.svg");This is useful for preloading critical SVGs during app initialization so the first render is instant.
<svg-isolate> supports four loading strategies controlled by the loading attribute.
Fetches the SVG immediately when the element connects to the DOM.
<svg-isolate src="icon.svg" loading="eager" />Waits for the DOMContentLoaded event before fetching. Useful when the SVG is not critical for the initial render.
<svg-isolate src="icon.svg" loading="defer" />Fetches during the browser's idle time using requestIdleCallback. Falls back to defer if the browser does not support it.
<svg-isolate src="icon.svg" loading="idle" />Note:
requestIdleCallbackis not supported in Safari stable (May 2026). The component automatically falls back todeferin that case.
Fetches the SVG only when the element enters the viewport, using IntersectionObserver. Ideal for SVGs below the fold.
<svg-isolate src="icon.svg" loading="lazy" />You can control when the load is triggered with lazy-margin and lazy-threshold:
<svg-isolate
src="icon.svg"
loading="lazy"
lazy-margin="200px"
lazy-threshold="0.5"
/>lazy-margin— extends the viewport boundary before triggering the load. Accepts any valid CSS margin value (e.g.200px,10%).lazy-threshold— percentage of the element that must be visible before triggering (0 to 1). Default is0.
SVGIsolate.defaults.loading = "lazy";<svg-isolate> supports srcset to serve different SVG files depending on the component's rendered width, similar to how native <img srcset> works.
Each candidate requires a width descriptor (w) representing the intrinsic width the SVG was designed for.
If no descriptor is provided, the candidate defaults to 0w.
<svg-isolate srcset="icon-300.svg 300w, icon-600.svg 600w, icon-900.svg 900w" />src and srcset are mutually exclusive. If srcset is present, src is ignored entirely — srcset always takes priority.
<!-- only srcset is used, src is ignored -->
<svg-isolate src="icon.svg" srcset="icon-300.svg 300w, icon-600.svg 600w" />This also applies when attributes change dynamically — if srcset is set at any point, src stops being considered until srcset is removed.
const el = document.querySelector("svg-isolate");
el.srcset = "icon-300.svg 300w, icon-600.svg 600w"; // src ignored from now on
el.srcset = null; // src is considered againThe component measures its own rendered width and picks the smallest candidate whose intrinsic width covers it:
component width: 450px
candidates: 300w, 600w, 900w
→ 300w < 450 — does not cover
→ 600w ≥ 450 — covers ✓ → selected
If the component is wider than all candidates, the largest is used as a fallback.
The selection runs once on connect, and again on every resize if responsive is enabled.
By default the component resolves the candidate once on connect. Add the responsive attribute to keep listening for size changes and swap the SVG automatically on resize:
<svg-isolate
srcset="icon-300.svg 300w, icon-600.svg 600w, icon-900.svg 900w"
responsive
/>Swaps are debounced to avoid excessive fetches during resize. Previously loaded candidates are served from the in-memory cache instantly.
SVGIsolate.defaults.responsive = true;Warning
XSS Risk on Untrusted SVGs:
If you are loading SVGs from untrusted user uploads or external user-generated sources, always enable the sanitize attribute and configure a secure sanitizer like DOMPurify.
While static <script> tags are blocked by DOMParser, inline event attributes (e.g., onload, onmouseover, onclick) will still execute inside the Shadow DOM when triggered by interaction or page cycles, opening viable XSS vectors (including dynamic code execution via import()).
<svg-isolate> renders SVG files inside a shadow DOM using DOMParser and appendChild. When rendering raw SVG, the browser enforces the following security and encapsulation behaviors:
<script>tags are never executed — the browser does not evaluate scripts inserted viaDOMParser+appendChild.
- Inline HTML event attributes (such as
onload,onmouseover,onclick) will execute inside the Shadow DOM when the respective user interaction or lifecycle event triggers them. - This represents a viable XSS vector, as malicious actors can inject payloads that execute arbitrary JavaScript or even dynamically import external scripts:
<rect onmouseover="import('./hack.js').then(mod => mod.hacking())" ... />
- To safely purge these attributes, you must use the
sanitizefeature.
- CSS inside
<style>tags is fully encapsulated by the shadow DOM — selectors likebody,p, ordivcannot escape and affect the rest of the parent page.
Set a static sanitizer function before any component renders. It receives the raw SVG string and returns the cleaned string. If not set, sanitization is skipped even when the sanitize attribute is present.
import DOMPurify from "https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs";
SVGIsolate.sanitizer = (raw) => {
return DOMPurify.sanitize(raw, {
USE_PROFILES: { svg: true },
FORBID_TAGS: ["style", "script"],
FORBID_ATTR: ["style"],
});
};The sanitizer runs after the fetch and before renderSVG, so the cache always stores the raw unsanitized string.
Controls whether the sanitizer is applied to a specific instance. Has no effect if SVGIsolate.sanitizer is not set.
<!-- sanitize this instance -->
<svg-isolate src="icon.svg" sanitize />
<!-- leave this one unsanitized -->
<svg-isolate src="icon.svg" />el.sanitize = true;
el.sanitize = false;To sanitize all instances without adding the attribute to each one:
SVGIsolate.defaults.sanitize = true;The SVG rendered inside <svg-isolate> lives in a shadow DOM, so external CSS cannot reach it directly. The component provides a few ways to interact with it.
Sets the viewBox attribute on the inner <svg> element. Useful for cropping or reframing the SVG coordinate system without modifying the source file.
<svg-isolate src="icon.svg" viewBox="0 0 100 100" />el.viewBox = "0 0 50 50";Changing this attribute dynamically updates the rendered SVG immediately without triggering a reload.
Sets the preserveAspectRatio attribute on the inner <svg> element. Controls how the SVG scales within its viewport.
<svg-isolate src="icon.svg" preserveAspectRatio="xMidYMid meet" />el.preserveAspectRatio = "xMinYMin slice";Changing this attribute dynamically updates the rendered SVG immediately without triggering a reload.
Adds a part attribute to the inner <svg> element, making it accessible via ::part() from external CSS.
<!-- expose with default part name "svg" -->
<svg-isolate src="icon.svg" expose-svg />
<!-- expose with a custom part name -->
<svg-isolate src="icon.svg" expose-svg="my-icon" />/* default name */
svg-isolate::part(svg) {
fill: red;
transform: rotate(45deg);
}
/* custom name */
svg-isolate::part(my-icon) {
fill: red;
}Note:
::part()gives access to the<svg>tag itself. Its children (path,circle, etc.) remain encapsulated and cannot be targeted from outside. Use CSS custom properties to style internals.
SVGIsolate.defaults.exposeSVG = true; // exposes with default part name 'svg'
SVGIsolate.defaults.exposeSVG = "custom-name"; // exposes with a custom part nameCSS custom properties penetrate the shadow DOM boundary, making them the most flexible way to style SVG internals.
Define the custom property on the component and consume it inside the shadow DOM styles:
svg-isolate {
--svg-fill: red;
--svg-stroke: blue;
}// when defining the component, inject a style that consumes the custom properties
SVGIsolate.define("svg-isolate", {
raw: `
svg * {
fill: var(--svg-fill, currentColor);
stroke: var(--svg-stroke, none);
}
`,
});This approach works for any CSS property regardless of shadow DOM encapsulation.
| Attribute | Description |
|---|---|
src |
Path to the SVG file. Triggers a reload when changed. Ignored if srcset is present |
srcset |
Comma-separated srcset candidates. Takes priority over src. Triggers a reload when changed |
preserveAspectRatio |
Forwarded directly to the inner <svg> without triggering a reload |
viewBox |
Forwarded directly to the inner <svg> without triggering a reload |
width |
Sets style.width on the host element. Accepts any valid CSS length (e.g. 200px, 50%, 10rem) |
height |
Sets style.height on the host element. Accepts any valid CSS length |
| Attribute | Default | Description |
|---|---|---|
base |
/ |
Base path or URL to prepend to the src. |
loading |
eager |
Loading strategy. One of eager, defer, idle, lazy |
responsive |
false |
Enables automatic candidate swapping on resize |
no-cache |
false |
Disables in-memory caching for this instance |
sanitize |
false |
Enables sanitization before rendering |
lazy-margin |
0px |
Viewport margin before triggering lazy load |
lazy-threshold |
0 |
Visibility ratio before triggering lazy load (0 to 1) |
expose-svg |
— | Exposes the inner <svg> via ::part(). Accepts an optional custom part name |
| Attribute | Description |
|---|---|
fetching |
Present while the SVG is being fetched. Removed once the fetch completes |
ready |
Present when the SVG has been successfully rendered |
ready-links |
Present when all external stylesheets have finished loading |
Use these attributes to drive CSS transitions or show loading states while the component initializes.
/* show a spinner while fetching */
svg-isolate[fetching] {
background: url('spinner.svg') center / 24px no-repeat;
}svg-isolate {
opacity: 0;
transition: opacity 0.3s;
}
svg-isolate[ready] {
opacity: 1;
}svg-isolate:not([ready-links]) {
opacity: 0;
}
svg-isolate[ready-links] {
opacity: 1;
}| Event | Description |
|---|---|
fetching |
Fired every time a fetch is about to start — on load, on src/srcset changes, and on srcset candidate swaps |
ready |
Fired every time an SVG is successfully rendered — on load, on src/srcset changes, and on srcset candidate swaps |
ready-links |
Fired once when all external stylesheets injected via links have finished loading |
el.addEventListener("fetching", (e) => {
const { src, resolved } = e.detail;
// src — the raw value from the src/srcset attribute
// resolved — URL object with the fully resolved href
});No detail. The SVG is already in the shadow root when the event fires.
el.addEventListener("ready", (e) => {
const svg = e.target.shadowRoot.querySelector("svg");
});el.addEventListener("ready-links", (e) => {
const { results } = e.detail;
// results — array of settled outcomes, one per <link> injected via `links`
// each entry: { link: HTMLLinkElement, href: string, status: "loaded" | "error" }
});For full API documentation including properties, methods, return types and parameters, see docs/api.md.
MIT
