@svelte-put/inline-svg

GitHub Github

solution for inlining SVGs in svelte land

@svelte-put/inline-svg @svelte-put/inline-svg @svelte-put/inline-svg @svelte-put/inline-svg

Still on Svelte 4? See the old docs site here.

Introduction

Existing solutions for inline SVGs in Svelte land often rely on component, which proves painful when it comes to custom styling or event handling. This package attempts to achieve a more minimal alternative using Svelte action (runtime) and Svelte preprocessor (compile time).

@svelte-put/preprocessor-inline-svg has been merged into version 4 of @svelte-put/inline-svg for a single coherent package. See Migration Guides if you previously used Svelte preprocessor for compile time solution.

Installation

npm install --save-dev @svelte-put/inline-svg
pnpm add -D @svelte-put/inline-svg
yarn add -D @svelte-put/inline-svg

Runtime - Dynamic SVGs - Svelte Action

This strategy is useful when:

  • you don't know in advance what SVGs to inline until runtime (after app/site is loaded or data is fetched in browser), or
  • if your SVG is large in size and only conditionally rendered.

For static icons and pictograms, consider the compile-time strategy instead.

Quick Start

The Svelte logo SVG on the right is dynamically fetched via network at runtime upon page load. Notice in the source code below, only width (or height) needs to be specified. By default, inlineSvg will calculate the other dimension the keep the aspect ratio.

<script>
	import { inlineSvg } from '@svelte-put/inline-svg';

	let { src = 'https://raw.githubusercontent.com/sveltejs/branding/master/svelte-logo.svg' } =
		$props();
</script>

<!-- add attributes and styling as with any other HTML element -->
<svg use:inlineSvg={src} width="100" class="svelte" />

<style>
	svg.svelte {
		filter: drop-shadow(0 0 0.5rem var(--color-svelte));
	}
</style>

inlineSvg only works if used on <svg> for obvious reason.

Attributes and Inner HTML

Attributes provided to the svg element where inlineSvg is placed on will replace existed ones from the original SVG. On the contrary, its inner HTML will be completely replaced.

Take the following SVG as an example:

<svg viewBox="0 0 24 24" width="24" height="24" stroke-width="2">
  <!-- truncated original svg innerHTML -->
</svg>

And inlineSvg is used as follows:

<svg use:inlineSvg={'https://example.com/original.svg'} height="16" stroke-width="1">
  <!-- some innerHTML -->
</svg>

The resulting SVG at runtime will be:

<svg viewBox="0 0 24 24" width="16" height="16" stroke-width="1">
  <!-- truncated original svg innerHTML -->
</svg>

If you have a use case where it is useful to append/prepend the innerHTML of the original SVG rather than replacing it, please raise an issue over at github. For now, let's keep things simple.

Customization

The inlineSvg action takes either a string as the remote SVG url, or a config object with the following interface:

/** verbose config for `inlineSvg` action */
export interface InlineSvgActionConfig {
  /** svg remote URI */
  src: string;
  /** cache policy for use in fetch from svg `src` */
  cache?: Request['cache'];
  /**
   * automatically calculate dimensions from the available attributes
   * of both the local SVG element (on which action is used) and the remote SVG
   *
   * For example, if you specify only `width` to the local SVG element, the
   * height will automatically be calculated from the remote SVG
   *
   * For this to work, width & height must be "extractable" from the remote element,
   * that is, the remote SVG must either have the `viewBox` or both `width` and `height` attributes that
   * is in the same unit.
   */
  autoDimensions?: boolean;
  /** optionally transform the SVG string fetched from remote source before inlining */
  transform?: (svg: string) => string;
}

Compile Time - Static SVGs - Svelte Preprocessor

This strategy is useful for SVGs that live in your repo as static assets. It only runs at compile time by Svelte preprocessor (via Vite plugin) during development, and therefore has zero runtime footprint. The idea is to enable the following...

<svg inline-src="google/info"> <!-- innerHTML inlined at compile time --> </svg>

...which allows additional styling and attributes to be added idiomatically as with any other HTML element. This is different from current solutions that I know of for inlining SVGs in Svelte land, which require either runtime logics or a component-oriented strategy.

Alternatively, for dynamic SVGs that are fetched at runtime, consider using the runtime strategy instead.

This library makes no implication that you should or should not use SVG to render icons. For some cases, it is helpful to do so, especially in conjunction with customizable color scheme. For others, different strategies such as icon font or CSS-only icons (see Icons in Pure CSS by Anthony Fu) might have more benefits.

Setup

Given the following vite.config.js and filesystem...

import path from 'path';
import { inlineSvg } from '@svelte-put/inline-svg/vite';

/** @type {import('vite').UserConfig} */
const config = {
  plugins: [
    inlineSvg([
     [
        {
          directories: 'src/assets/icons',
          attributes: {
            class: 'icon',
            width: '20',
            height: '20',
          },
        },
        {
          directories: 'src/assets/pictograms',
        },
      ],
    ]),
    sveltekit(),
  ],
};
export default config;
src/assets
    |
    |-- icons
         |-- svelte.svg
         |
         |-- google
               |-- arrow-right.svg
         |-- simpleicons
               |-- github.svg
    |
    |-- pictograms
         |-- diagram.svg

...we can now do...

<!-- this will have width="20" height="20" as specified in the config -->
<svg inline-src="svelte" />

<!-- nested -->
<svg inline-src="google/arrow-right.svg" />
<!-- .svg can be omitted -->
<svg inline-src="simpleicons/github" />

<!-- with custom attributes -->
<svg inline-src="diagram" width="100" height="100" />

<!-- alternatively, you can provide a per-case path that is relative to the current source file -->
<svg inline-src="./local-icon.svg" />

<!-- if the source svg is not found for any of the above, an error will be thrown -->

Attributes and Inner HTML

Attributes provided to the svg element where inline src attribute (inline-src by default) is specified will replace existed ones from the original SVG. On the contrary, its inner HTML will be completely replaced.

Take the following SVG as an example:

<svg viewBox="0 0 24 24" width="24" height="24" stroke-width="2">
  <!-- truncated original svg innerHTML -->
</svg>

And inlineSvg is used as follows:

<svg inline-src="./local-icon.svg" height="16" stroke-width="1">
  <!-- some innerHTML -->
</svg>

The resulting SVG at runtime will be:

<svg viewBox="0 0 24 24" width="24" height="16" stroke-width="1">
  <!-- truncated original svg innerHTML -->
</svg>

Notice that the width attribute is not automatically calculated for you in the output above. Make sure to provide both dimensions. This behavior differs from the runtime strategy, which automatically calculates the missing dimension to preserve aspect ratio. The rationale is that preprocessor operates at compile time with static SVGs, so it trusts your intention. If you think otherwise, feel free to open a discussion.

If you have a use case where it is useful to append/prepend the innerHTML of the original SVG rather than replacing it, please raise an issue over at Github. For now, let's keep things simple.

Customization

By default the Vite plugin / Svelte preprocessor can be setup with no config at all, in which case SVG source paths are resolved relative to the Svelte source file the inline src attribute (inline-src as default) is specified in.

export function inlineSvg(
 uSources?: PreprocessorSourceDefinition | PreprocessorSourceDefinition[],
 uConfig?: PreprocessorConfig,
): import('svelte/compiler').PreprocessorGroup | import('vite').Plugin;

Note that path alias is not supported! For example, "$lib/src/assets/..." will not work.

Source Definitions

The first parameter to Vite plugin / Svelte preprocessor can be either a single object, or an array of such (as seen in Setup), helpful for organizing SVG sources into different directories.

/** sources for the inline svg */
export type PreprocessorSourceDefinition = {
	/**
	 * directories relative to which the svg source path will be resolved
	 */
	directories?: string[] | string;
	/**
	 * default attributes to add to the svg element, will override the attributes from the svg source,
	 * but be overridden by the attributes from the element itself (in svelte source)
	 */
	attributes?: Record<string, string>;
};

When source parameter is an array of config objects, the following applies: there can be only one config object without the directories option, taken as the default config and will be used for all SVG sources not covered by other config objects (all relative SVG paths). If multiple config objects without directories are provided, an error will be thrown during development. If none is provided, the internal default config is used.

During build, each SVG source will then be searched top down in the config until a match is found, or else an error will be thrown. Relative SVGs (relative to current Svelte source file) always has the highest priority and will use the default config as described above.

Preprocessor Options

The second parameter to the Vite plugin / Svelte preprocessor provides customization to the processor itself. Their corresponding interfaces are as follow:

/** global options for configuring behaviors of the inline-svg preprocessor */
export type PreprocessorConfig = {
  /** attribute to get the svg source from, default to `inline-src` */
  inlineSrcAttributeName?: string;
  /** whether to keep the inline src attribute after compilation, default to `false` */
  keepInlineSrcAttribute?: boolean;
};

/** configuration for the vite plugin that wraps inline-svg preprocessor */
export type VitePluginConfig = PreprocessorConfig & {
  /**
   * output path for generated type definition for inline src attribute.
   * Defaults to `null` (no generation).
   * Set to `true` to use the default `src/preprocess-inline-svg.d.ts` path.
   */
  typedef?: string | null | true;
};

Typescript Support

Specify typedef option to let Vite plugin automatically generate type definition for the inline src attribute. This is useful to enable type checking and auto-completion in your editor.

import path from 'path';
import { inlineSvg } from '@svelte-put/inline-svg/vite';

/** @type {import('vite').UserConfig} */
const config = {
  plugins: [
    inlineSvg([
      [/** truncated source config as in Setup */],
      { typedef: true },
    ]),
    sveltekit(),
  ],
};
export default config;
/** DO NOT EDIT! This file is generated by @svelte-put/inline-svg vite plugin */
declare module 'svelte/elements' {
	export interface SVGAttributes {
		'inline-src'?:
			`./${string}`
			| `../${string}`
			| 'svelte'
			| 'google/arrow-right'
			| 'simpleicons/github'
			| 'diagram';
	}
}
export {};
src/assets
    |
    |-- icons
         |-- svelte.svg
         |
         |-- google
               |-- arrow-right.svg
         |-- simpleicons
               |-- github.svg
    |
    |-- pictograms
         |-- diagram.svg

As mentioned in Preprocessor Options, when typedef set to true, the vite plugin will write the type definition to src/preprocess-inline-svg.d.ts by default. You can specify a custom path if needed.

Avoid setting inlineSrcAttributeName to data-* in this case since it would be "swallowed" by the broader typedef from svelte/elements.

Using Bare Svelte Preprocessor

The wrapper Vite plugin adds some improvements to developer experience by watching the specified directories from your source config and triggering page reload when there is addition/removal/change to them. It also provide optional typedef generation as seen in previous section.

If your setup doesn't allow Vite, however, you can do so by importing the preprocessor directly from @svelte-put/inline-svg/preprocessor.

import inlineSvg from '@svelte-put/inline-svg/preprocessor';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: [
    inlineSvg(/** truncated config */),
    // other preprocessors
  ],
};
export default config;

Limitations

The inlineSvg preprocessor only works in Svelte markup, i.e in the template part of Svelte files. The following will not work:

<script>
  let html = `<svg inline-src="path/icon"></svg>`;
</script>

{@html html}

Similarly, the preprocessor does not support inline src attribute as a variable. The following will not work:

<script>
  const icon = 'path/icon';
</script>

<svg inline-src={icon} />

This is because it is difficult for the preprocessor to statically analyze a variable to determine its immutability at compile time, i.e a variable is meant to be changed. In these case, some alternatives are:

  • use if-else statements to render different svg element with static inline src attribute as literal string, or
  • use the runtime strategy to dynamically inline SVGs in browser.

If you have an idea for improvements, please raise an issue over at github. Thanks!

Frequently Asked Questions

Q: Why should I care about runtime vs build time?
A: Javascript! Runtime requires Javascript. Without it, users will not see your SVG. On the other hand, build time does the work beforehand, so SVGs are already there in the initial HTML.

Q: When to use which?
A: If you do not know in advance what SVGs to inline, or if your SVG is huge but only conditionally rendered: use Svelte action runtime strategy. Otherwise, especially for static icons and pictograms, use Svelte preprocessor compile-time strategy.

Q: Do I really need this package?
A: No! Use <img> when possible. My initial use case is to be able to change properties of the SVG, especially color. But that can potentially be done with mask-image. So consider your use case before adding another dependency.

Migration Guides

From Preprocessor

If you are coming @svelte-put/preprocess-inline-svg, simply update the import path of the preprocessor in your svelte.config.js:

import inlineSvg from '@svelte-put/preprocess-inline-svg';
import inlineSvg from '@svelte-put/inline-svg/preprocessor';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: [
    inlineSvg(/** truncated config */),
    // other preprocessors
  ],
};
export default config;

Additionally, consider switching to the Vite plugin wrapper and enabling typedef option, if your setup allows it, for better developer experience. For more information, see Using Bare Svelte Preprocessor.

import path from 'path';
import { inlineSvg } from '@svelte-put/inline-svg/vite';

/** @type {import('vite').UserConfig} */
const config = {
  plugins: [
    inlineSvg([/** truncated source setup */], {
      typedef: true,
    }),
    sveltekit(),
  ],
};
export default config;
import inlineSvg from '@svelte-put/inline-svg/preprocessor';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: [
    inlineSvg(/** truncated config */),
    // other preprocessors
  ],
};
export default config;

Prior Art


Happy inlining SVGs! 👨‍💻

Edit this page on Github