Best way to import SVG icons into a Svelte app
Asked Answered
U

8

51

I have about 80 custom SVG icons that I'm importing into a Svelte front-end app. Building on https://github.com/sveltejs/template, it's built with Rollup and includes Typescript, Tailwind, and all the modern goodies.

enter image description here

The dilemma is how to add the icons into the app. As SVGs, the icons are short XML text strings that do not exceed 2kB.

Option 1: as image assets

  1. Upload all the icons as foo.svg into public/assets/icons.
  2. Create a svelte component <Icon type="foo' /> that displays an the icon using <img src="foo.svg>.

This approach means that the icons are not part of the code.

Benefits: icons can be dynamically loaded by frontend code on demand. No need to bundle all icons into app code.

Cons: slow page load if there are a lot of new icons, and the browser has to fetch a dozen 1kB files. Deploying the app as a PWA means we need to manually tell it to cache the icons beforehand.

Option 2: as part of the app build

  1. Use something like https://github.com/cristovao-trevisan/svelte-icon or https://github.com/codefeathers/rollup-plugin-svelte-svg to directly import each icon into code: import Home from './icons/home.svg';
  2. Create a svelte component that selects the right imported component or SVG string and displays it.

Here, the icons are bundled as text strings with the app itself.

Benefits: Icons are delivered as part of the app bundle. Caching is unnecessary. Possible to dynamically modify some of the icon code e.g. colors, viewBox, etc on load.

Cons: It's unnecessary to include all icons in the app to reduce time to first byte. Can't do bundle splitting, etc. without adding more complexity. Makes the rendering slower because Javascript code needs to first turn a string into an SVG instead of just loading an image.

Questions

  • It seems that building icons in tthe app is a better way from this analysis, but have I missed something?
  • Does the calculus change if the "icons" are detailed images that are 50-100kB instead of the tiny strings here?
Until answered 5/2, 2021 at 19:44 Comment(5)
Option 3: create the SVG client-side with a <svg-icon> Custom Element from a string holding only d path data; see iconmeister.github.io - I never did the Svelte version because Svelte handles the <svg-icon> native element just fine. I have converted over 7000 icons from different IconSets - The JS code is only 800 Bytes GZippedBister
PS. if you have 50-100kB for "icons" there is something wrong with the "designer". I did 52 Playingcards in 16kB and 300+ Country flags in 29kBBister
I always just inline them (option 2) but make sure to optimize all your svg, those sizes are indeed very large, you can use a tool like svgomg for the optimizing. If the icons come from a designer their tool might also be able to export them, just inspect the svg and if you see lot's of crap instead of plain paths it's a sign they are not optimized.Felt
Focus with Mobile 3G, speed must go first (I prefer the option 3 by Danny, like other said the size seems big for just svg)Anselm
@StephaneVanraes are you saying that 1kb is large or 50kb is large?Until
A
46

The following approach has these advantages:

  • One central point to maintain all your icons for your app
  • Reduced network requests for fetching SVG icons
  • Reusable icons throughout the app without having duplicate svg elements

Have a dedicated Icon.svelte component setup like this:

<script>
  export let name;
  export let width = "1rem";
  export let height = "1rem";
  export let focusable = false;
  let icons = [
    {
      box: 24,
      name: "save",
      svg: `<g stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8"/><path d="M7 3v5h8"/></g>`
    },
    {
      box: 32,
      name: "trash",
      svg: `<path d="M12 12h2v12h-2z" /><path d="M18 12h2v12h-2z" /><path d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20z" /><path d="M12 2h8v2h-8z" />`
    }
  ];
  let displayIcon = icons.find((e) => e.name === name);
</script>
<svg
  class={$$props.class}
  {focusable}
  {width}
  {height}
  viewBox="0 0 {displayIcon.box} {displayIcon.box}">
    { @html displayIcon.svg }
</svg>

Typescript version with strong typed names

<script lang="ts">
    export let name: keyof typeof icons;
    export let width = '1rem';
    export let height = '1rem';
    export let focusable: string | number | null | undefined = undefined;
    let icons = {
        delete: {
            box: 32,
            svg: `<path d="M 15 4 C 14.476563 4 13.941406 4.183594 13.5625 4.5625 C 13.183594 4.941406 13 5.476563 13 6 L 13 7 L 7 7 L 7 9 L 8 9 L 8 25 C 8 26.644531 9.355469 28 11 28 L 23 28 C 24.644531 28 26 26.644531 26 25 L 26 9 L 27 9 L 27 7 L 21 7 L 21 6 C 21 5.476563 20.816406 4.941406 20.4375 4.5625 C 20.058594 4.183594 19.523438 4 19 4 Z M 15 6 L 19 6 L 19 7 L 15 7 Z M 10 9 L 24 9 L 24 25 C 24 25.554688 23.554688 26 23 26 L 11 26 C 10.445313 26 10 25.554688 10 25 Z M 12 12 L 12 23 L 14 23 L 14 12 Z M 16 12 L 16 23 L 18 23 L 18 12 Z M 20 12 L 20 23 L 22 23 L 22 12 Z"></path>`
        }
    } as const;
    let displayIcon = icons[name];
</script>

<svg
    class={$$props.class}
    {focusable}
    {width}
    {height}
    viewBox="0 0 {displayIcon.box} {displayIcon.box}">{@html displayIcon.svg}</svg
>

You can then use it like so:

<Icon name="trash" class="this-is-optional" />
Anticosti answered 10/2, 2021 at 11:6 Comment(11)
Using this with typescript currently throws typeerror for the "name" prop as that seems to be reserved when using react/tsx and svelte converts to tsx for typescript. Also throws type error for focusable attribute but that seems possible to change to "false" as a string containing false rather than boolean type.Kumler
This is a great solution! I was wondering how, if possible, I could automatically grab the parent elements font size instead of passing in the width height in as a prop? is this even possible? Regardless awesome solution!Ambrotype
Instead of an array, maybe use an object with the icon names as the keys, so you don't have to do a find each time an icon is rendered.Arbitrary
You can make SVG images responsive via the preserveAspectRatio and viewBox attributes: developer.mozilla.org/en-US/docs/Web/SVG/Attribute/… Everything you need to know about scaling SVG images: css-tricks.com/scale-svgGenuflect
There's an disadvantage to this approach too - it's inability to preview the icon just by opening it from Explorer or Finder as an SVG file. Editing/cleaning up manually an SVG after adding it to the Svelte component becomes much harder.Lobule
@EnricoBorba do you have an example of converting to object and getting rid of find?Vittle
@Vittle icons = {'save': {box:24, svg: '...'}, 'trash': {...}} and then displayIcon = icons[name]Misdemeanor
Great approach, but the icons seem to disappear when you navigate away from the route containing the icon component & return.Passifloraceous
I added a typescript versionCoherence
This is great. One improvement I made is to keep the svg files in $lib/assets/... but import the raw svg text like so: github.com/sveltejs/kit/discussions/…. Then set the svg: value in the map to that imported raw text. This still lets you open those files in a viewer and see what they look like.Exercitation
You should put the icons into <script context="module"> instead. Otherwise every <Icon /> instance redefined the icon set which wastes a lot of memory.Giffy
B
43

You can just change the file extension to .svelte and import an SVG as a normal component.

Bodine answered 5/6, 2021 at 5:26 Comment(6)
This also works well for SSR (I mean SvelteKit).Careerism
And what about doing that dynamically? As when you need that when the SVG depends on the locale's valueChris
@SebastianSastre {@if blahblah} :)Bodine
@Bodine ah but I mean not for when to render it but for loading it conditionally. I give you a case: You have a multilingual optimized page and one of the images is language dependent. You want your SvelteKit component loading the right locale dependent resource only once. How do you do that?Chris
@SebastianSastre, with dynamic import.Bodine
simple ! I love it !Brilliancy
T
24

Would you like to keep the SVG icon files unchanged?

The ?raw import option of Vite could be used for this purpose to import the raw content of the SVG icon file. This option is described here https://vitejs.dev/guide/assets.html#importing-asset-as-string

And with the svelte {@html } option it will be outputted in raw html.

It will combine how option 1 in the question explains to not have the icons as part of the code and still import an icon to make it part of the app build like described in option 2 of the question.

The main benefit will be that the SVG icon library could be kept original with *.svg extensions. For example:

import SearchIcon from '$lib/icon/search.svg?raw';
{@html SearchIcon}

Only the SVG icons that are required for the app's functionality will be included in the code. This ensures that the app remains lightweight and efficient.

Although I would als consider the scenario to rework the SVG files into .svelte files like proposed in another answer. I've just proposed the Vite ?raw function in case it is preferred to keep the icon files untouched. As always with the use of @html make sure the icons are "trusted" html files and could not be (re)generated. So a file path like $lib/icon/*.svg with original SVG files is a safe solution.

Travel answered 16/4, 2023 at 16:59 Comment(1)
Damn that's super handy! This should be the first solution. Thanks Bob.Shanley
F
10

another way is to use a symbol defs file (ex: icons.svg) in your public folder. then in your code do something like this :

Icon.svelte

<script>
    export let name;
    export let width = "1.5rem";
    export let height = "1.5rem";
    export let focusable = false;
</script>

<svg class={$$props.class} {focusable} {width} {height}>
    <use href={`/icons.svg#${name}`} />
</svg>

icons.svg

<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
        <symbol id="icon-warning" viewBox="0 0 20 20">
            <path d="M19.511 17.98l-8.907-16.632c-0.124-0.215-0.354-0.348-0.604-0.348s-0.481 0.133-0.604 0.348l-8.906 16.632c-0.121 0.211-0.119 0.471 0.005 0.68 0.125 0.211 0.352 0.34 0.598 0.34h17.814c0.245 0 0.474-0.129 0.598-0.34 0.124-0.209 0.126-0.469 0.006-0.68zM11 17h-2v-2h2v2zM11 13.5h-2v-6.5h2v6.5z">
            </path>
        </symbol>
    </defs>
</svg>

App.svelte

<Icon name="icon-warning" />

this way you use one http call to load svg file. and then just use the part you need in markup.

Floating answered 9/8, 2021 at 13:52 Comment(1)
While I like this method for it's pretty straight use of HTML, I couldn't get it to work consistently. Sometimes targeting the id with the use reference would work, sometimes it would use the entire <path> as the id.Negligible
S
9

Using like {@html SVG_CODE} for rendering a svg code from another component or variable is a Bad choice . This sometimes gives risk of XSS attack . Specially when the image comes from external source. check : https://github.com/sveltejs/svelte/issues/2545

Svelte doesn't perform any sanitization of the expression inside {@html ...} before it gets inserted into the DOM. In other words, if you use this feature it's critical that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks.(source)

I think using plugin like rollup-plugin-svelte-svg would be a better choice

1.Install using npm

npm i -D rollup-plugin-svelte-svg

2.Simply call svelteSVG before svelte in your rollup config.


export default {
    
    plugins: [
        svelteSVG({
            // optional SVGO options
            // pass empty object to enable defaults
            svgo: {}
        }),
    ],
    ...
}

3.You can then import svg in your JS thusly:

<script>
    import Logo from "./logo.svg";
</script>

<Logo width=20 />

Or

Open your svg image using text editor, copy all the code and paste in a file name with .svelte extension .

for example , icon.svg file contains :

<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clip-rule="evenodd"></path></svg>

Copy it in a icon.svelte file

<script></script>
<style></style>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clip-rule="evenodd"></path></svg>

When you want use icon.svelte component

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

If you want you can pass optional class

then in icon.svelte

<script></script>
<style></style>
<svg class="{$$props.class}" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clip-rule="evenodd"></path></svg>

When you want to call icon.svelte component

<script>
import Icon from "$lib/icon.svelte"
</script>
<style></style>
<Icon class="h-6 w-6" />

Sitka answered 30/12, 2021 at 18:55 Comment(5)
-1 "This gives risk of XSS and other attacks." That's a misleading claim. There's an XSS risk only if you're rendering unsanitized user input via {@html ...}; which in the case of SVG icons on your own server, you aren't. So no, there are no security issues with using {@html svgString} here.Schaffel
i updated the answer , any advice would be appreciated , thanksSitka
Thanks. But still, mentioning XSS here is misleading, because when you're rendering something that's coming from a trusted origin (like SVGs on your own server), XSS isn't a concern. This is pointed out even in the GitHub issue you linked to yourself — here. The {@html} feature in Svelte is in fact precisely meant for these kinds of scenarios.Schaffel
thanks , does every image comes from a trasted source? i am not so sure about that , think like a simple svg editing site where site render svg based on user input , how that can trusted . And i pointed concern about XXS because svelte official documentation pointed this , i do think they know better than me , svelte.dev/tutorial/html-tagsSitka
"... HTML that comes from sources you don't trust". I think it is absolutely fine to mention XSS here, with the caveat given. How frequently do developers build systems that load SVGs from non-trusted sources? Not often. But it is in the "not often" that security vulnerabilities hide.Streaky
T
3

A working solution is to hold the SVGs in a separate IconService.js:

export const chevron_down = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-chevron-down\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>";

export const chevron_right = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-chevron-right\"><polyline points=\"9 18 15 12 9 6\"></polyline></svg>";

This can easily be imported and used within svelte @html function.

<script>
    import {chevron_down, chevron_right} from 'IconService';
</script>

{@html chevron_down}
Theona answered 9/2, 2021 at 10:53 Comment(2)
What exactly does this help with?Until
Thought the question is: Best way to import SVG icons into a Svelte appTheona
M
1

A Svelte Component with SVG Icons inside:

<script>
  export let i
  export let stroke = 3
</script>

<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" 
  stroke-width={stroke} stroke="currentColor" class={`w-6 h-6 ${$$props.class}`}>
  
  {#if i == 'search'}
  <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
  
  {:else if i == 'plus'}
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />

  {:else}
  <path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
  {/if}
</svg>

Use like this:

<Icon i='search'/>
<Icon i='plus' stroke={5} class="w-10 h-10 text-slate-500"/>

Benefits

All icons are collected in a single file. The SVG-Paths are compiled as HTML-Code and not as inline strings. This also displays any errors in the SVG in the IDE. When you add a new Icon you just have to copy the contents of the SVG into this file.

Misdemeanor answered 8/9, 2022 at 15:33 Comment(0)
K
1

Programmatically loading the svg as string from one module (iconsProvider.js) which is exporting all svgs as constants. So basically, with every new svg, you only need to export it from iconprovider.js. And then pass icon.svelte the right iconName prop.

icon.svelte

<script lang="ts">
import * as iconsProvider from "../lib/iconsProvider"

export let iconName: string
</script>
{@html iconsProvider[ iconName ]}

iconsProvider.js

export const icon1 = `<svg ...>`
export const icon2 = `<svg ...>`

Hope this helps.

Karyogamy answered 29/9, 2022 at 10:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.