Style your SVGs with CSS. No build step. No framework lock-in. ~3.5 KB gzipped.
SVGInject replaces <img> elements with inline <svg> so you can target every path, circle, and group with CSS - colors, animations, hover effects, dark mode, all of it. One line of code, works everywhere.
Using v1? v2 is a drop-in upgrade - same API, no code changes needed. You get bug fixes, better accessibility, and a full test suite. Only downside: no more IE support. See what changed.
Vanilla - download or copy the file:
<script src="svg-inject.min.js"></script>npm - versioned, update with npm update:
npm install @iconfu/svg-inject<script src="node_modules/@iconfu/svg-inject/dist/svg-inject.min.js"></script>Bundler - import and expose globally:
import { SVGInject } from '@iconfu/svg-inject';
window.SVGInject = SVGInject;Then in your HTML:
<img src="icon.svg" onload="SVGInject(this)" />
<!-- The SVG gets injected and is styleable! -->Without onload - inject from JavaScript instead (better for strict CSP):
document.addEventListener('DOMContentLoaded', () => {
SVGInject(document.querySelectorAll('img.injectable'));
});React, Vue, or Svelte? See Frameworks.
SVGInject works best when you don't have a build step - or don't want one for your SVGs:
- WordPress, CMS, static sites - add a
<script>tag, done - Server-rendered pages - PHP, Rails, Django, any backend template
- Dynamic / third-party content - HTML injected at runtime, CMS editors, widgets
- Prototyping - style SVGs with CSS without setting up tooling
- Multi-framework projects - one solution across jQuery, React, vanilla, whatever
SVGInject is a runtime library. It loads and injects SVGs in the browser. No build step, no bundler, no Node.js required.
SVGInject is simpler and lighter than framework-specific packages for styling static SVGs with CSS. We don't ship framework packages - SVGInject is one function, here's how to wire it up.
If you need loading states, error boundaries, or dynamic src changes, look at tools like react-inlinesvg - or build your own on top of SVGInject's API.
React - handler:
import { SVGInject } from '@iconfu/svg-inject';
const svgInject = (e) => SVGInject(e.currentTarget);
<img src="icon.svg" onLoad={svgInject} />Vue - custom directive:
// main.js
import { SVGInject } from '@iconfu/svg-inject';
app.directive('svg-inject', {
mounted(el) { el.onload = () => SVGInject(el); }
});<img src="icon.svg" v-svg-inject />Svelte - action:
<script>
import { SVGInject } from '@iconfu/svg-inject';
function svgInject(node) { node.onload = () => SVGInject(node); }
</script>
<img src="icon.svg" use:svgInject />~3.5 KB gzipped. Zero runtime dependencies. Tree-shakeable ESM. Ships with full TypeScript definitions.
SVGInject automatically sets the right ARIA attributes based on your <img>:
alt="descriptive text"- setsrole="img"andaria-labelon the SVGalt=""(decorative) - setsrole="none"andaria-hidden="true"titleattribute - becomes a<title>child element inside the SVG- ARIA ID references (
aria-labelledby,aria-describedby, etc.) are updated when IDs are made unique
<!-- Meaningful icon -->
<img src="chart.svg" alt="Sales by quarter" onload="SVGInject(this)" />
<!-- Becomes: <svg role="img" aria-label="Sales by quarter"> ... </svg> -->
<!-- Decorative divider -->
<img src="divider.svg" alt="" onload="SVGInject(this)" />
<!-- Becomes: <svg role="none" aria-hidden="true"> ... </svg> -->When multiple SVGs on the same page share IDs (common with gradient or clipPath definitions), SVGInject automatically makes all IDs unique by appending --inject-N suffixes. References in url(), href, xlink:href, and ARIA attributes are updated to match.
Each SVG URL is fetched once and cached for the page lifetime. Subsequent injections of the same URL reuse the cached SVG string but parse a fresh DOM element, so ID uniquification and sanitization are always applied.
SVGInject can strip <script> elements, <foreignObject>, on* event handler attributes, and javascript:/data: URIs before injection. Enable with sanitize: true. See Security for details.
Safe to import in Node.js, Next.js, Nuxt, SvelteKit, and any server-side environment. All DOM access is deferred to function call time - no top-level window or document references.
All attributes are copied from <img> to <svg> with these rules:
- Excluded:
src,alt,onload,onerror titlebecomes a<title>child elementaltbecomesaria-label(or triggers decorative mode if empty)styleis merged with the SVG's existing inline style (img values win on conflicts)- Case-sensitive SVG attributes like
viewBoxandpreserveAspectRatioare correctly mapped from their lowercased HTML form
Set copyAttributes: false to disable and handle it yourself in beforeInject.
SVGInject(img, options?): Promise<void>Injects the SVG for one or many <img> elements. Accepts a single element, an array, or a NodeList. Returns a Promise that resolves when all injections complete.
SVGInject.setOptions(options): voidSets global default options for all subsequent injections.
SVGInject.create(name, options?): SVGInjectFunctionCreates an independent instance with its own cache and options. The name parameter sets the global variable name (used for onload attribute binding) and the flash-prevention CSS selector.
SVGInject.err(img, fallbackSrc?): voidError handler for onerror on <img> elements. Optionally sets a fallback src.
| Property | Type | Default | Description |
|---|---|---|---|
useCache |
boolean |
true |
Cache SVG content per URL for the page lifetime |
copyAttributes |
boolean |
true |
Copy attributes from <img> to <svg> |
makeIdsUnique |
boolean |
true |
Append --inject-N suffix to all IDs to prevent collisions |
sanitize |
boolean |
false |
Strip <script>, <foreignObject>, event handlers, and dangerous URIs before injection |
injectStyleTag |
boolean |
true |
Inject a <style> tag to hide images before injection, preventing unstyled image flash. Set to false if you have a strict CSP |
beforeLoad |
(img) => string | void |
Hook before loading. Return a string to override the URL | |
afterLoad |
(svg, svgString) => string | SVGSVGElement | void |
Hook after loading. Modify the SVG or return a new one. Called once per URL when caching is enabled | |
beforeInject |
(img, svg) => Element | void |
Hook before injection. Return an element to inject instead | |
afterInject |
(img, svg) => void |
Hook after injection | |
onAllFinish |
() => void |
Called when all elements in a batch are done | |
onFail |
(img, status) => void |
Called on failure. Status is 'LOAD_FAIL', 'SVG_INVALID', or 'SVG_NOT_SUPPORTED' |
When using onload, the browser may briefly show the raw <img> before injection replaces it. SVGInject prevents this by default - it injects a CSS rule that hides injectable images until injection is complete.
This requires style-src 'unsafe-inline' in your Content Security Policy. If you have a strict CSP, disable it and add the rule to your own stylesheet instead:
SVGInject.setOptions({ injectStyleTag: false });img[onload^="SVGInject("] { visibility: hidden; }<img src="icon.svg"
onload="SVGInject(this)"
onerror="SVGInject.err(this, 'fallback.png')" />Or use the onFail hook:
SVGInject.setOptions({
onFail(img, status) {
console.error('Injection failed:', status); // 'LOAD_FAIL', 'SVG_INVALID', or 'SVG_NOT_SUPPORTED'
}
});SVG files can contain scripts. SVGInject includes built-in protections you can opt into:
- Built-in sanitization. Enable with
SVGInject.setOptions({ sanitize: true })or per call withSVGInject(img, { sanitize: true }). This strips<script>,<foreignObject>,on*event handlers, andjavascript:/data:URIs before injection, catching the most common XSS vectors. - For untrusted SVGs (user uploads, third-party URLs), consider adding DOMPurify in the
afterLoadhook for comprehensive protection. - Same-origin policy applies. SVGs are loaded with
fetch(). Cross-origin SVGs require CORS headers on the server. - CSP note. The
onload="..."attribute requiresscript-src 'unsafe-inline'(or a nonce/hash). If you use a strict CSP, call SVGInject from JavaScript instead (see Quick start).
v2 is API-compatible with v1. Breaking changes:
| Change | Migration |
|---|---|
| IE9-11 no longer supported | Stay on v1.x for IE |
| Decorative images handled correctly | alt="" now sets role="none" + aria-hidden="true" |
alt converted to aria-label |
Accessibility improvement |
style merged instead of overwritten |
Better behavior when both img and SVG have inline styles |
Chrome, Firefox, Safari, Edge - all modern versions.