Dynamically Loading SVG Icons with Vue.js
In this article we’re going to explore two approaches for dynamically loading SVG icons with Vue.js. We’ll use the wonderful vue-svgicon package as a foundation for our SVG icon workflow.
If you want to take a closer look at the example code, you can find it on GitHub. Or you can check out a live example of the code hosted on Netlify.
Installing vue-svgicon
There are multiple ways of how to integrate SVG icons into a website, but because we’re using Vue.js, we want to use an approach which enables us to use components, like we’re used too with Vue.js, to load icons. Luckily, the vue-svgicon
package makes it possible to automatically convert .svg
files into Vue components.
npm install vue-svgicon --save
After we’ve installed vue-svgicon
, we can use it to automatically generate icon components of SVG files for us. The best way to run the script is, to add a new line to the scripts
section of our package.json
file.
{
"scripts": {
"icons": "vsvg -s src/assets/icons -t src/components/icons",
"prebuild": "npm run icons",
"build": "node build/build.js"
}
}
The icons
script is responsible for creating Vue components from static .svg
files located in the src/assets/icons
directory. We also add a prebuild
script, which automatically runs before the build
script, to start the icons
script before building the page. Keep in mind though, that you have to run npm run icons
manually every time you add a new icon during development.
Configuring vue-svgicon
After we’ve installed vue-svgicon
, we must configure it in the src/main.js
file of our application.
// src/main.js
import Vue from "vue";
import * as svgicon from "vue-svgicon";
import App from "./App";
// We install `vue-svgicon` as plugin
// and configure it to prefix all CSS
// classes with `AppIcon-`.
Vue.use(svgicon, {
classPrefix: "AppIcon-",
});
new Vue({
el: "#app",
render: (h) => h(App),
});
For this demo application, I downloaded three icons from flaticon.com, ran them through SVGOMG to save some bytes, and put them into the src/assets/icons
directory.
npm run icons
Now we can generate the Vue icon components by running the command above. Don’t forget to add the directory which contains the automatically generated icon components to your .gitignore
file, to prevent them from ending up in your Git repository.
# .gitignore
src/components/icons
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
Dynamically loading icons
Although, most SVG icons are quite small in file size, their size can add up. To prevent them slowing down the initial page load, we can use dynamic loading to lazy load icons which are not visible on initial page load.
Approach 1: Default component + watch
Let’s start with the default way of using icons generated with vue-svgicon
and enhance it with lazy loading powered by dynamic imports and the Vue.js watch
feature.
<svgicon v-if="showMagicHat" name="magic-hat" height="3em"></svgicon>
<button @click="showMagicHat = !showMagicHat">Toggle Magic Hat Icon</button>
In the template code above, you can see that we’re defining a svgicon
component tag which is only rendered if showMagicHat
is true
. The name
property is used to define which icon should be rendered, in this case we’re rendering the icon with the file name magic-hat.svg
. The button beneath the icon toggles the value of showMagicHat
between true
and false
.
export default {
name: "App",
data() {
return {
showMagicHat: false,
};
},
watch: {
// This method is triggered whenever
// the value of `showMagicHat` changes.
showMagicHat(value) {
if (value)
import(
/* webpackChunkName: "svgicon-magic-hat" */ "./components/icons/magic-hat"
);
},
},
};
In the code above, you can see that we’re using a watcher function named showMagicHat()
to trigger a dynamic import of the magic-hat
icon component. By specifying a webpackChunkName
, we can control the name of the chunk file which is generated by webpack. For the webpack chunk name feature to work, make sure that you’re using the [name]
placeholder in the webpack chunkFilename
setting (you can look at the example config on GitHub).
If we run this code in production mode, you can see in the network tab of the developer tools of your browser, that the icon is not loaded initially but it is lazy loaded whenever the button is clicked the first time.
Approach 2: Wrapper component
Although the first approach is perfectly fine, we still can do better, and add a layer of abstraction to make things a little bit easier to reuse.
<app-icon v-if="showMusic" name="music" size="l" fill></app-icon>
<button @click="showMusic = !showMusic">Toggle Music Icon</button>
The template code above looks pretty much the same as what we’ve seen before. The only major difference is, that we’re using an app-icon
tag, instead of the default svgicon
tag, to load the icon component.
import AppIcon from "./components/AppIcon";
export default {
name: "App",
components: {
AppIcon,
},
data() {
return {
showMusic: false,
};
},
};
In this code snippet, you can see, that we’re not loading anything dynamically and we don’t have to use a watcher function for dynamic loading. Instead we’re importing and using a new AppIcon
component. Let’s take a closer look at the implementation of the AppIcon
component in the following code snippet.
<template>
<svgicon
:class="{
[$options.name]: true,
[`${$options.name}--${size}`]: size,
}"
:name="name"
>
</svgicon>
</template>
<script>
export default {
name: "AppIcon",
props: {
name: {
type: String,
required: true,
},
size: {
type: String,
},
},
created() {
// The `[request]` placeholder is replaced
// by the filename of the file which is
// loaded (e.g. `AppIcon-music.js`).
import(/* webpackChunkName: "AppIcon-[request]" */ `./icons/${this.name}`);
},
};
</script>
<style>
.AppIcon {
display: inline-block;
height: 1em;
color: inherit;
vertical-align: middle;
fill: none;
stroke: currentColor;
}
.AppIcon--fill {
fill: currentColor;
stroke: none;
}
.AppIcon--s {
height: 0.5em;
}
.AppIcon--m {
height: 1em;
}
.AppIcon--l {
height: 3em;
}
</style>
The AppIcon
component you can see above, is basically a simple wrapper around the svgicon
component. Let’s walk through the code.
In the template you can see, that we’re using the components name, which is stored in $options.name
, to define the CSS class. Additionally to the default CSS class, we’re also adding a size class, which can be controlled by passing one of the three sizes (s
, m
or l
) as a property to the component. Because we’re using the svgicon
tag as root tag of the AppIcon
component, all additional properties are directly passed to the svgicon
component, so we can use all of the properties provided by the svgicon
component.
In the components created()
method, we’re dynamically loading the icon component which matches the given name
property. Because the created()
method is not executed as long as the component isn’t initialized, the icon isn’t loaded until the component is rendered.
In the style section of the component, we’re adding some default styles which are recommended by vue-svgicon
and we add the necessary styling to make the size classes work.
Downsides of dynamic loading
Like with most things in life, dynamically loading SVG icons also has its downsides. Especially if you’re displaying a lot of (different) icons initially, without requiring any user interaction for them to show up, loading all those icons in separate HTTP requests is most likely slower than loading them all in one JavaScript bundle.
The wrapper component approach I’ve shown in this article isn’t very flexible in that regard: all icons are always loaded dynamically. No matter if you’re showing them instantly or after a certain user interaction.
Recap
Depending on your situation, you might consider to use one of the two approaches I’ve shown in this article. The first approach, of using the Vue.js watch
feature to dynamically load icons if needed and bundle them with the main bundle otherwise, is more flexible but also more complicated.
Using a wrapper component for your icons makes them pretty straightforward to use. There might be a performance hit with making a lot of separate HTTP requests in certain situations. Although, the default Vue.js webpack template comes with the CommonsChunkPlugin preconfigured, which should take care of such situations.
Aside from dynamically loading icons only if they are needed, using a wrapper component has two other benefits. First of all, because we’ve added a layer of abstraction, we’re more flexible if we decide to use some other tool instead of vue-svgicon
in the future. And second, this approach makes it possible to put the icon styling where it belongs: in a separate (icon) component.