How to Embed, Inline, and Reference SVG Files in Svelte

I recently needed to work with SVGs in Svelte, and had trouble finding comprehensive guides on solutions, especially those that allow for the SVG to be completely inlined, so that you can use things like fill: currentColor, or CSS rules. This post tries to cover some of the most useful solutions I found during my research, complete with step-by-step instructions and code snippets.

Table of Contents

Placing in Public and Avoiding Bundling

If you don’t care about inlining the SVG, and are OK with an external embed, the easiest way to get an SVG into a Svelte component is to simply bypass the bundler and use the /public directory.

For example, you could place your file at: /public/assets/dog.svg, and then your Svelte file could be as simple as:

<img src="/assets/dog.svg" />

In general, there are some important drawbacks to this approach however:

  • Paths are not checked by the compiler / bundler, or even the IDE
    • This makes typos a little too easy, or situations where a file gets renamed and no one catches the change
  • Assets are not optimized
    • Any file you stick in /public is going to end up in the build output. By contrast, if you use the bundler (see other approaches I outline), assets that are not used will not get bundled into the build output
  • You cannot inline SVGs this way
    • This method allows you to embed SVGs via <img> tags, but it does not allow you to inline the SVG contents directly into the DOM
    • This prevents a lot of direct manipulation of the SVG from working, which might be a deal-breaker if you need to animate parts of it, change nested colors, or pass through CSS variables

Using a Bundler Plugin

As is common with many frameworks, one way to solve this problem is to have the bundler take care of reading in the image files, transforming them, and letting them be imported into components. Since the Svelte starter template ships with Rollup by default, that is the bundler I’ll be discussing.

To solve this with Rollup, there are three plugins I’ll suggest as solutions.

I’m not going to cover it in-depth in this post, but for webpack, check out their docs on asset modules.

rollup-plugin-svg [or] rollup-plugin-inline-svg

These two plugins are similar in how they work, although rollup-plugin-inline-svg has more configuration options.

Once you have installed the plugin you have chosen and updated your rollup config, for actually importing and using the SVG, both require similar code.

Inline SVG embedding (this code works for either plugin):

<script lang="ts">
    import dogSvgDecoded from '../assets/dog.svg';
</script>

<main>
    {@html dogSvgDecoded}
</main>

Embedding as external embed, via image tag + data URI:

  • For embedding via data URI:
    • rollup-plugin-svg
      • Change base64 config option to be true
      • In component, import and then bind as <img src={importedSvgStr}>
    • rollup-plugin-inline-svg
      • Since the plugin is focused on inlining, there is no config option for escaping / base64 encoding.
      • The workaround is to decode the string inside the component – this is essentially the reverse of the @rollup/plugin-image workaround:
      <script lang="ts">
          import dogSvgDecoded from '../assets/dog.svg';
          const dogSvgEncoded = `data:image/svg+xml,${encodeURIComponent(dogSvgDecoded)}`;
      </script>
      
      <main>
          <img src={dogSvgEncoded} />
      </main>
      

@rollup/plugin-image

The @rollup/plugin-image is the most popular of the plugins, and can support more formats than just SVGs (also supports JPG, PNG, etc.), but also comes with an important limitation.

The limitation is that it converts the contents to a base64 string (or HTML encoded entities string, in the case of SVGs) and then expects you to use it as a Data URI – for most image formats this is no problem (and actually preferred), but for SVGs, this also prevents them from being directly inlined, which subsequently prevents CSS rules from working. If you are fine with that limitation, or want to use the workaround (more on this in a bit), it is fairly simple to set up.

  1. Modify your rollup config to pull in the the new plugin
  2. Activate the plugin within the config (plugins: [image()])
  3. Now, in any Svelte component where you want to use an SVG:
    1. Import the file, while assigning it to a variable
    2. Bind the variable you assign it to, as src={myVar} on an image element

Example component:

<script lang="ts">
    import DogSvgEncoded from '../assets/dog.svg';
</script>

<main>
    <img src={DogSvgEncoded} alt="Cute dog graphic" />
</main>

@rollup/plugin-image: Inline SVG Workaround

As noted above, a limitation with @rollup/plugin-image is that it is designed to be used with the src attribute on image tags, not for embedding the raw text of a SVG into the DOM. This is why it Base64 encodes the value of binary image files, and escapes / encodes HTML entities in the value of SVG files.

However, with some extra work we can turn the encoded value it gives us back into the original raw SVG string:

<script lang="ts">
    import DogSvgEncoded from '../assets/dog.svg';

    const dogSvgDecoded = decodeURIComponent(DogSvgEncoded).replace('data:image/svg+xml,', '');
</script>

<main>
    {@html dogSvgDecoded}
</main>

This works, but should be avoided (in favor of any of the alternatives outlined in this post), for three reasons:

  • You are shifting computational work from a build-server onto the users’ browser, which is… not great.
  • This is adding extra steps – why escape characters only to decode immediately?
  • It actually increases bandwidth used; it takes extra characters (longer string, more bytes) to escape HTML entities
    • For a tiny test SVG, the original version used 3,988 characters, whereas the encoded one grew to 4,285.

Svite / Vite

Currently, using SVG bundling plugins with Vite or Svite is a little tricky.

  • If you are using Svite, I would recommend just using svelte-inline-svg components, or directly inlining SVGs into Svelte SFC. If Svite is updated to internally use Vite 2.0+, instead of v1, then the below will apply.

  • If you are using Vite (2.0+), SVG inlining should eventually work out of the box; it currently has an open issue against it (#1204), but there is a PR in progress that should address it.

Directly Inlining

If the SVG file is small enough and it makes sense for your needs, you can actually just copy and paste the raw SVG contents into a Svelte SFC. This is similar to the another solution in this post, renaming entire SVG files to .svelte files.

Make sure you remove any declarations, like <?xml version="1.0" encoding="UTF-8"?>, before inlining

Saving as Svelte Files

As suggested in the comments on this S/O question, one option for importing SVGs and rendering them inline is by renaming the .svg file to .svelte and then importing and using as a normal Svelte component. This works because of how flexible Svelte is with its templating / SFC system.

However, there are some catches. The main one is that if you try this with some SVG files, you might see this error:

ParseError: Expected valid tag name

This might happen because your .svg file contains (at the top) something like:

<?xml version="1.0" encoding="UTF-8"?>

This is an XML declaration (a subset of an XML Prolog). The short summary of what is going on here is that:

  • Svelte doesn’t like the prolog
  • It shouldn’t be allowed anyways if the SVG is going to be inlined into the DOM – HTML spec allows / recommends only one declaration per doc, at the top of the page
  • It is not even necessary to include in the SVG, even if the file is going to be used as a normal SVG external embed

As the declaration is not even necessary for standalone SVG files, the easy fix is to simply remove the declaration line from your file before or after changing the extension to .svelte.

The other solutions outlined in this post deal with omitting the declaration automatically, which is another reason to prefer them over converting your SVGs to Svelte files

Side-note: Dealing with TypeScript

If you are using TypeScript with Svelte, you might see the following error when trying to import an SVG, even if you have properly configured your bundler:

Cannot find module ‘../dog.svg’ or its corresponding type declarations.ts(2307)

If our bundler is going to give us a string when we import the SVG path, we can tell TS that this is the case by creating an ambient declaration file and typing the file pattern.

ambients.d.ts:

declare module '*.svg' {
    const content: string;
    export default content;
}

Svelte-Inline-SVG and other AJAX Based Solutions

The plugin svelte-inline-svg plugin, and some similar plugins, solve the inlining issue while avoiding bundlers, by taking a somewhat unusual approach. The package is essentially a Svelte component that requests your SVG file as a network request, then parses the response and inlines the nodes.

This can work for a lot of situations, but also comes with an important caveat; the SVG files must be able to be fetched by the component at run-time, or in the case of SSR, at build-time. In practicality, this means that SVG assets would need to be placed in /public, and not /src.

Other Solutions

There are other solutions out there, that I either have missed or have not taken the time to discuss in this post.

If you have found a solution that you think would be helpful to share, please do so below in the comments (or ping me via message, and I’ll update the post).

Leave a Reply

Your email address will not be published. Required fields are marked *