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
- Using a Bundler Plugin
- Directly Inlining
- Saving as Svelte Files
- Side-note: Dealing with TypeScript
- Svelte-Inline-SVG and other AJAX Based Solutions
- Other Solutions
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
- Any file you stick in
- 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
- This method allows you to embed SVGs via
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.
- rollup-plugin-svg or rollup-plugin-inline-svg
- These are both similar, and both are good if you need to inline a lot of SVGs
- @rollup/plugin-image
- This supports more image formats, but is less friendly for inlining
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 betrue
- In component, import and then bind as
<img src={importedSvgStr}>
- Change
- 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-svg
@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.
- Modify your rollup config to pull in the the new plugin
- Activate the plugin within the config (
plugins: [image()]
) - Now, in any Svelte component where you want to use an SVG:
- Import the file, while assigning it to a variable
- 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).