How to config for using svg file in sveltekit?
Asked Answered
P

5

5

To import and use svg file in sveltekit I refer to this article https://riez.medium.com/svelte-kit-importing-svg-as-svelte-component-781903fef4ae By the way, when I finally input the code

<svelte:component this={Logo} />

I got the error like below

<svelte:component this={...}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules

I wish someone help me with this problem.

Precaution answered 30/4, 2021 at 16:33 Comment(2)
I solve this problem temporarily by using img tag. Such as <img src={Logo} /> But I still want to solve this component issue because it is useful in array rendering and so on.Precaution
it is not possible to change color using <img> tag. But svg file's color can be changed using just background-color and mask with an empty <div>. More info [#24933930Amends
U
10

The Issue

I know this is an old question, but unfortunately, I also run into the same bug, and the answer here doesn't help a lot.

  1. using a <img> is great if the svg styles are self-contained, but once you need to size or colour them relative to the parent element (like currentColor this stop working)
  2. creating a new Svelte file can be great, but I prefer that my assets are in one folder as normal.
    why?
    • for readable purposes: .svg/.png/.jpg for assets. and real Components with .svelte
    • performance purposes: https://mcmap.net/q/413175/-where-to-put-images-with-sveltekit
    • time consuming: for every asset to have to add: you need manually create a new file .svelte, and paste the .svg code. this is easy for 2/3 of icons but what happens if you have 50/80+ icons? is time-consuming.
  3. using an external library for this isn't necessary,
    because we can implement this by ourselves using less code and native vite/svelte code.

    So let's do it below

Solution

As I said, guys don't install that library because sveltekit+vite handles that natively easily (unless you need to)

  1. now in lib/ create a folder called assets/ where you will insert all your .svg/.png/.jpg (not inside static/ for performance reasons, because if inside lib vite bundler will handle caching)

  2. let's say we want to import myIcon.svg:

<script>
  import myIcon from "$lib/assets/myIcon.svg?raw"
</script>

{@html myIcon}

?raw is returning us the text inside the file (see https://vitejs.dev/guide/features.html#static-assets)
instead of the URL link, like it was happening before (for default assets return have ?url in vite)

As you see, use 2 lines of native svelte code to make it work fine.

this works also when you nest your svg inside another and you are using currentColor

in most cases this @html is great,
But if your images come from input then don't use @html or you can be hacked
(or use it but with the input values sanitized)


EDIT:

  • if you want to make this like a component library use this code for your component:
<script>
  export let src;

  $: isSvg = !!src.endsWith(".svg");                             // we use $: because you may dynamically change the src so the component needs to be reactive. if you are sure that it won't be changed it will be great to use instead `const`
  $: svgRoute = isSvg ? src.replace(".svg", ".svg?raw") : null;  // you can also do `${src}?raw`
</script>

{#if isSvg}
  {#await import(svgRoute)}
    <div>loading...</div>       <!-- default fallback, can be a loading spinner or anything else that the user will see while waiting for the loading -->
  {:then value}
    {@html value.default}       <!-- the svg code will be inject here -->
  {/await}
{:else}
  <img src={src} />            <!-- if the src is only a normal photo then use native html `<img>`'s src attribute -->
{/if}
  • if you want to simplify even more this import myIcon from "$lib/assets/myIcon.svg?raw" you can use my other answer on that topic

How can I simplify my imports when using SvelteKit with Vite bundler, like $lib/?

so by using that component it will be like this:

<script>
  import Icon from "$lib/Icon.svelte"
</script>

<Icon src="$assets/foo.svg" />
<Icon src="$assets/bar.svg" />
<Icon src="$assets/hello.png" />

<div class="text-blue-500">
  <Icon src="$assets/nested.svg" />
</div>

EDIT: ⚠️ fix of latest vitejs's bug

lately the above solution won't work anymore when using the npm run build by throwing this error:

TypeError: Failed to fetch dynamically imported module

https://github.com/vitejs/vite/issues/11804

so use this instead for the svg part

<script lang="ts">
  export let src: string;

  $: isSvg = !!src.endsWith(".svg");         
  $: svgRoute = `${src}?raw`;

  function fetchSvg(svgRoute: string) {
    return new Promise((resolve) => {
      fetch(svgRoute)
      .then(response => response.text())
      .then(svg => resolve(svg))
    })
  }
</script>

{#if isSvg}
   {#await fetchSvg(svgRoute)}
     <div>loading...</div>
   {:then value}
     {@html value}
   {/await}
{:else}
  <img src={src} />
{/if}

Useless answered 6/4, 2023 at 14:10 Comment(0)
G
8

There is a SvelteKit plugin for inlining SVG:s. You can use the plugin in three different ways:

  1. As a Svelte Component
  2. As URL (e.g for use in an img tag)
  3. As raw text (e.g for use in {@html rawText})

https://www.npmjs.com/package/@poppanator/sveltekit-svg

Gardel answered 25/5, 2021 at 9:6 Comment(0)
J
4

In svelte-kit You can fix it by trying

<img src={YourSVGComponent} />

It worked for me.

Jolo answered 2/5, 2021 at 6:12 Comment(1)
If you use it like this, css cannot interact with the SVG element.Fleeta
L
3

Looking through the article it seems to be solving a problem that doesn't exist in a way that is much more elaborate than needed.

In Svelte you can make a .svelte component that only contains SVG markup (inline), then use the svelte:component tag as you would with any other content.

Parent.svelte

<script>
  import Circle from './Circle.svelte'
</script>

<svelte:component this={Circle} />

Circle.svelte

<svg viewbox="0 0 200 200">
  <circle cx="100" cy="100" r="20"/>
</svg>

Here's a REPL showing how to switch between components that only have SVG in them.

You can even add stuff to the SVG components to make them dynamic since it's just markup like shown in this REPL.

Leavitt answered 30/4, 2021 at 23:48 Comment(3)
I think it is only for svelte. But I used the sveltekit and I think it is the problem of config. Your answer can't solve the problem. Because it was the method I used before. And it didn't act and I want to know the method how to solve it.Precaution
I used this in Svelte Kit and it still works.Leavitt
Here's a repo you can clone and run showing this method 100% does work with Svelte-kit github.com/JHethDev/svg-ssvelte-kit SVG is just markup, I can't imagine how your Svelte-kit config would prevent you from using markup in a Svelte file.Leavitt
T
1

I used the component solution Laaouatni Anas suggests, utilizing sveltekit+vite's ability to handle raw files and import them as the actual raw contents themselves.

The Problem

One caveat of Laaouatni Anas's component solution (not the original answer) was that it if I imported the component on the client side even with ssr it sometimes would need to fetch the actual svg via a call. Which is great in terms of loading up the bulk of everything first, but it takes a small amount of time to fetch the svg. So there would usually be a slight delay in rendering the svg. Which did not look good when first loading up the site.

Another thing to keep in mind you should be careful of this method, as using the @html tag to load in content that isn't scanned potentially opens up highjacking purposes to malicious users.

My Solution

Instead of loading up the svg via a call and passing args for the filepath to the svg I created a seperate file to store the icons raw:

import chevronRightSolid from '$lib/assets/chevron-right-solid.svg?raw';

export const rawIcons = {
    chevronRightSolid: chevronRightSolid
};

As you add new icons you will need to update the file and add them, but compared to creating a component file for every single icon and exporting that this feels like a more sane answer.

Then create the actual icon component, importing the rawIcons, which allows it to just be cached once so you don't store duplicates of the raw svg code.

<script lang="ts">
    import { rawIcons } from '$lib/resources/icon.resources';
    export let icon: keyof typeof rawIcons;
    export let width = '20px';
    export let height = '20px';
    export let fill = '#000';
</script>

<div
    style="--width:{width};
--height: {height};
--fill:{fill};">
    {@html rawIcons[icon]}
</div>

<style>
    div {
        width: var(--width);
        height: var(--height);
        fill: var(--fill);
        display: flex;
        justify-content: center;
        align-items: center;
    }
</style>

Keep in mind

Laaouatni Anas's solution with lots of svgs might be better for certain use cases. My use case was that I did not need ssr and I was building the project for a capacitor svelte app. This enabled me to just bundle everything and send that all to the user basically all at once. If you send the user the RawIcons file and you have hundreds of icons it could take a little longer to load, increasing load times.

Edit

To fix a bug regarding safari where default width/height doesn't appear to be 100% you might need to either modify every single svg component that you download. Or you could add this script to modify the component:

<script lang="ts">
    import { rawIcons } from '$lib/resources/icon.resources';
    export let icon: keyof typeof rawIcons;
    export let width = '20px';
    export let height = '20px';
    export let fill = '#000';
    function addStylesToSVG(svgString: string) {
        // Ensure the SVG string starts with an <svg> tag
        if (!svgString.startsWith('<svg')) return svgString;
        return (
            `<svg style='width: ${width}; height: ${height}; fill: ${fill};' ` + svgString.slice?.(4)
        );
    }
</script>

{@html addStylesToSVG(rawIcons[icon])}

<style>
</style>

Or depending on your project, if you don't have other svgs you're worried about. You could just add a global style for svgs to default it to what chrome does

:global(svg) {
        width: 100%;
        height: 100%;
}
Tweeze answered 29/8 at 20:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.