Skip to content

svg-jar/plugin

Repository files navigation

SvgJar

@svg-jar/plugin

npm version CI License

An unplugin for importing SVGs as components. Supports sprite sheets, inline SVGs, and raw file exports across Vite and Rollup.

Install

pnpm add -D @svg-jar/plugin

Setup

Vite

// vite.config.ts
import svgJar from '@svg-jar/plugin/vite';

export default {
  plugins: [svgJar({ target: 'ember' })],
};

Rollup

// rollup.config.ts
import svgJar from '@svg-jar/plugin/rollup';

export default {
  plugins: [svgJar({ target: 'ember' })],
};

Usage

Sprite mode (default)

SVGs are collected into a sprite sheet and rendered via <use href>. The sprite file is emitted with a content-hashed filename for cache busting.

import Arrow from './icons/arrow.svg';

Named sprites

Group SVGs into separate sprite sheets with the ?sprite=name query:

import Circle from './icons/circle.svg?sprite=shapes';

This creates a separate shapes-<hash>.svg sprite file containing only the icons assigned to it.

Inline mode

Embed the full SVG markup directly in the component. No sprite sheet, no external request.

import Square from './icons/square.svg?unsafe-inline';

Caution

Inline SVGs are embedded in your JavaScript bundle, increasing parse time and preventing separate caching. Sprite mode (the default) is more efficient for most use cases. See What's unsafe about inline SVGs?

File mode

Export the SVG as a raw asset URL, like any other static file:

import logoUrl from './icons/logo.svg?file';

const img = document.createElement('img');
img.src = logoUrl;

Options

svgJar({
  // Framework target for component generation.
  // Default: 'dom'
  target: 'dom' | 'ember' | 'react' | 'preact' | 'vue' | 'solid' | 'web-component',

  // SVGO configuration.
  // true (default): use baseline config with sensible defaults
  // false: disable optimization entirely
  // object: custom SVGO config, deep-merged with baseline
  svgo: true,

  // Default sprite name for bare SVG imports.
  // Default: 'sprite'
  defaultSprite: 'sprite',

  // Replace non-none fill/stroke with currentColor globally.
  // Default: false
  currentColor: false,
});

Per-SVG modifiers

These can be combined with any import mode:

Query Effect
?current-color Replace fill/stroke with currentColor (opt-in per SVG)
?skip-current-color Preserve original colors (opt-out when global option is true)
// This icon inherits color from its parent's CSS color property
import Icon from './icons/icon.svg?current-color';

Targets

The target option controls what each SVG import exports. All targets support sprite, inline, file, and named sprite modes.

DOM

Factory functions that create SVG DOM elements. Each call returns a new element, so the same import can be inserted multiple times. An optional options object sets attributes and creates <title>/<desc> elements for accessibility.

import Arrow from './icons/arrow.svg';

document.body.appendChild(Arrow());

document.body.appendChild(
  Arrow({
    class: 'icon',
    'aria-label': 'Navigate forward',
    title: 'Forward arrow',
    desc: 'An arrow pointing to the right',
  }),
);

Ember

Glimmer components supporting ...attributes for attribute passthrough and {{yield}} for block content.

import Arrow from './icons/arrow.svg';

<template>
  <Arrow class="icon" aria-hidden="true">
    <title>Forward arrow</title>
    <desc>An arrow pointing to the right</desc>
  </Arrow>
</template>

React

React function components that accept all SVG props and support children for accessibility content.

import Arrow from './icons/arrow.svg';

function App() {
  return (
    <Arrow className="icon" aria-label="Navigate forward">
      <title>Forward arrow</title>
      <desc>An arrow pointing to the right</desc>
    </Arrow>
  );
}

Preact

Preact function components with the same API as React.

import Arrow from './icons/arrow.svg';

function App() {
  return (
    <Arrow class="icon" aria-label="Navigate forward">
      <title>Forward arrow</title>
    </Arrow>
  );
}

Vue

Vue render objects that accept all attributes and support the default slot for accessibility content.

<script setup>
import Arrow from './icons/arrow.svg';
</script>

<template>
  <Arrow class="icon" aria-label="Navigate forward">
    <title>Forward arrow</title>
    <desc>An arrow pointing to the right</desc>
  </Arrow>
</template>

Solid

Solid components that accept all SVG attributes and support children.

import Arrow from './icons/arrow.svg';

function App() {
  return (
    <Arrow class="icon" aria-label="Navigate forward">
      <title>Forward arrow</title>
      <desc>An arrow pointing to the right</desc>
    </Arrow>
  );
}

Web Component

Custom element classes that render SVGs in the light DOM. The user registers the element with customElements.define().

import SvgArrow from './icons/arrow.svg';

customElements.define('svg-arrow', SvgArrow);
<svg-arrow>
  <title>Forward arrow</title>
  <desc>An arrow pointing to the right</desc>
</svg-arrow>

Child elements (<title>, <desc>) are moved inside the rendered <svg> on connectedCallback.

TypeScript

Add the client types for your target to your tsconfig.json:

{
  "compilerOptions": {
    "types": ["@svg-jar/plugin/client/<target>"]
  }
}

Available client types:

Target Types path Default export type
dom @svg-jar/plugin/client/dom (options?) => SVGSVGElement
ember @svg-jar/plugin/client/ember ComponentLike<{ Element }>
react @svg-jar/plugin/client/react FC<SVGAttributes>
preact @svg-jar/plugin/client/preact FunctionComponent<SVGAttributes>
vue @svg-jar/plugin/client/vue Component
solid @svg-jar/plugin/client/solid Component<ParentProps<...>>
web-component @svg-jar/plugin/client/web-component typeof HTMLElement

Note

If your project also uses vite/client types, its *.svg declaration (which types SVGs as string) may conflict with the plugin's component types.

Custom module declarations

TypeScript can't match two wildcards in a module specifier, so named sprite queries (?sprite=name) and combined query params (?current-color&sprite=icons) aren't covered by the built-in declarations. Add your own in a .d.ts file included by your tsconfig:

// svg.d.ts (React example)
declare module '*.svg?sprite=icons' {
  import type { FC, SVGAttributes, ReactNode } from 'react';

  const Component: FC<SVGAttributes<SVGSVGElement> & { children?: ReactNode }>;
  export default Component;
}

For the dom target:

declare module '*.svg?sprite=icons' {
  import type { SvgOptions } from '@svg-jar/plugin/runtime/dom';

  const component: (options?: SvgOptions) => SVGSVGElement;
  export default component;
}

The ?file query always exports a string URL regardless of target, so no custom declaration is needed for combined file queries.

How it works

Build mode

  1. resolveId
    Intercepts .svg imports and parses query strings
  2. load
    Reads the SVG, optimizes with SVGO, parses via @eksml/xml, applies transforms (currentColor, strip dimensions), resolves embedded <use> and <image> references, registers symbols in the sprite registry
  3. transform
    Generates framework-specific component code with a sprite placeholder URL
  4. renderChunk
    Replaces placeholders with final sprite URLs, tracks which symbols are actually used (tree-shaking)
  5. generateBundle
    Assembles sprite sheets from used symbols only, resolves embedded refs to final asset URLs, emits sprite and file assets

Dev mode

In development, sprite sheets are not assembled. Each SVG is rendered as a self-contained inline sprite: the SVG content is wrapped in a <symbol> with a local <use href="#id"/> reference. Any SVGs referenced via <use href> are embedded as additional <symbol> entries in the same <svg>, so all references are local fragment refs that work cross-browser (Safari, Firefox, Chrome).

Inline mode (?unsafe-inline) embeds the raw SVG markup directly.

HMR is supported
Editing an SVG file triggers a hot update of all components that import it.

SVGO optimization

SVGO runs per-SVG during the load phase (not on assembled sprites) to prevent CSS cross-contamination between symbols. The baseline config:

  • Runs preset-default with ID preservation (needed for sprite symbol refs)
  • Inlines <style> tags to prevent cross-symbol CSS leaks
  • Strips <title> elements
  • Preserves path data and numeric values for visual fidelity

Runs in both dev and prod so behaviour is consistent.

Embedded reference resolution

SVGs containing <use href="other.svg"> or <image href="photo.png"> have their references resolved through the bundler's module graph:

  • <use> refs to other SVGs become sprite symbol references
  • <image> refs to SVGs are emitted as file assets
  • <image> refs to non-SVG files (PNG, etc.) are handled by the bundler's native asset pipeline

In dev mode, <use> SVG references are embedded as local <symbol> entries for cross-browser compatibility.

What's unsafe about inline SVGs?

The ?unsafe-inline query is intentionally named to make you think twice. In most cases, sprite mode (the default) is the better choice. Inline SVGs have real costs that aren't immediately obvious:

  • Larger bundles
    Every inline SVG adds to the JavaScript your users download and parse. SVG markup embedded in JavaScript is significantly more expensive to parse than the same markup served as HTML or as an external file.
  • No separate caching
    SVGs bundled in JS are invalidated whenever your code changes, even if the icons haven't changed. Sprite sheets are separate files with content-hashed filenames that stay cached independently.
  • Duplicate DOM nodes
    If the same inline SVG is rendered multiple times on a page, each instance creates a full copy of the markup in the DOM. Sprites use <use href> to reference a single shared <symbol>, avoiding duplication.
  • Slower rendering
    Frameworks that use a virtual DOM (React, Vue, etc.) pay additional costs creating and diffing VDOM nodes for every element in the SVG. Sprites sidestep this entirely.

Inline SVGs make sense in rare cases
Complex animations that target specific SVG elements, or SVGs that need to be fully self-contained. For icons and static graphics, sprites are more efficient.

For a thorough analysis of this problem, see Cynthia Rey's The state of SVGs on the Web.

Requirements

  • Node.js >= 20
  • Vite or Rollup

Acknowledgements

  • Ivan Volti for creating ember-svg-jar, which has been the standard tool for SVGs in Ember for over 10 years, big thanks to him and everyone who has maintained it since.
  • Cynthia Rey for vite-plugin-magical-svg, which directly inspired this plugin's approach to sprite-based SVG handling and embedded reference resolution. Her writing on the problems with inline SVGs shaped the design philosophy behind the design of this plugin.