.
font-size
and color
properties (just like an icon font).This is what we will build by writing two small, single-file components. There are a few specific requirements for this implementation, though I’m sure many of you wizards out there could rework this system for other frameworks and build tools:
npm install svg-inline-loader --save-dev
from the terminal to get started.To meet our requirement of not repeating SVG code for each instance of an icon on the page, we need to build an SVG “sprite.” If you haven’t heard of an SVG sprite before, think of it as a hidden SVG that houses other SVGs. Anywhere we need to display an icon, we can copy it out of the sprite by referencing the id of the icon inside a <use>
tag like this:
<svg><use xlink:href="#rocket" /></svg>
That little bit of code is essentially how our <SvgIcon>
component will work, but let’s go ahead create the <SvgSprite>
component first. Here is the entire SvgSprite.vue
file; some of it may seem daunting at first, but I will break it all down.
<!-- SvgSprite.vue -->
<template>
<svg width="0" height="0" style="display: none;" v-html="$options.svgSprite" />
</template>
<script>
const svgContext = require.context(
'!svg-inline-loader?' +
'removeTags=true' + // remove title tags, etc.
'&removeSVGTagAttrs=true' + // enable removing attributes
'&removingTagAttrs=fill' + // remove fill attributes
'!@/assets/icons', // search this directory
true, // search subdirectories
/\w+\.svg$/i, // only include SVG files
);
const symbols = svgContext.keys().map((path) => {
// get SVG file content
const content = svgContext(path);
// extract icon id from filename
const id = path.replace(/^\.\/(.*)\.\w+$/, '$1');
// replace svg tags with symbol tags and id attribute
return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>');
});
export default {
name: 'SvgSprite',
svgSprite: symbols.join('\n'), // concatenate all symbols into $options.svgSprite
};
</script>
In the template, our lone <svg>
element has its content bound to $options.svgSprite
. In case you’re unfamiliar with $options
it contains properties that are directly attached to our Vue component. We could have attached svgSprite
to our component’s data
, but we don’t really need Vue to set up reactivity for this since our SVG loader is only going to run when our app builds.
In our script, we use require.context
to retrieve all of our SVG files and clean them up while we’re at it. We invoke svg-inline-loader
and pass it several parameters using syntax that is very similar to query string parameters. I’ve broken these up into multiple lines to make them easier to understand.
const svgContext = require.context(
'!svg-inline-loader?' +
'removeTags=true' + // remove title tags, etc.
'&removeSVGTagAttrs=true' + // enable removing attributes
'&removingTagAttrs=fill' + // remove fill attributes
'!@/assets/icons', // search this directory
true, // search subdirectories
/\w+\.svg$/i, // only include SVG files
);
What we’re basically doing here is cleaning up the SVG files that live in a specific directory (/assets/icons
) so that they’re in good shape to use anywhere we need them.
The removeTags
parameter strips out tags that we do not need for our icons, such as title
and style
. We especially want to remove title
tags since those can cause unwanted tooltips. If you would like to preserve any hard-coded styling in your icons, then add removingTags=title
as an additional parameter so that only title
tags are removed.
We also tell our loader to remove fill
attributes, so that we can set our own fill
colors with CSS later. It’s possible you will want to retain your fill
colors. If that’s the case, then simply remove the removeSVGTagAttrs
and removingTagAttrs
parameters.
The last loader parameter is the path to our SVG icon folder. We then provide require.context
with two more parameters so that it searches subdirectories and only loads SVG files.
In order to nest all of our SVG elements inside our SVG sprite, we have to convert them from <svg>
elements into SVG <symbol>
elements. This is as simple as changing the tag and giving each one a unique id
, which we extract from the filename.
const symbols = svgContext.keys().map((path) => {
// extract icon id from filename
const id = path.replace(/^\.\/(.*)\.\w+$/, '$1');
// get SVG file content
const content = svgContext(path);
// replace svg tags with symbol tags and id attribute
return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>');
});
What do we do with this <SvgSprite>
component? We place it on our page before any icons that depend on it. I recommend adding it to the top of the App.vue
file.
<!-- App.vue -->
<template>
<div id="app">
<svg-sprite />
<!-- ... -->
</div>
</template>
Now let’s build the SvgIcon.vue
component.
<!-- SvgIcon.vue -->
<template>
<svg class="icon" :class="{ 'icon-spin': spin }">
<use :xlink:href="`#${icon}`" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
icon: {
type: String,
required: true,
},
spin: {
type: Boolean,
default: false,
},
},
};
</script>
<style>
svg.icon {
fill: currentColor;
height: 1em;
margin-bottom: 0.125em;
vertical-align: middle;
width: 1em;
}
svg.icon-spin {
animation: icon-spin 2s infinite linear;
}
@keyframes icon-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
</style>
This component is much simpler. As previously mentioned, we leverage the <use>
tag to reference an id inside our sprite. That id
comes from our component’s icon
prop.
I’ve added a spin
prop in there that toggles an .icon-spin
class as an optional bit of animation, should we ever need. This could, for example, be useful for a loading spinner icon.
<svg-icon v-if="isLoading" icon="spinner" spin />
Depending on your needs, you may want to add additional props, such as rotate
or flip
. You could simply add the classes directly to the component without using props if you’d like.
Most of our component’s content is CSS. Other than the spinning animation, most of this is used to make our SVG icon act more like an icon font¹. To align the icons to the text baseline, I’ve found that applying vertical-align: middle
, along with a bottom margin of 0.125em
, works for most cases. We also set the fill
attribute value to currentColor
, which allows us to color the icon just like text.
<p style="font-size: 2em; color: red;">
<svg-icon icon="exclamation-circle" /><!-- This icon will be 2em and red. -->
Error!
</p>
That’s it! If you want to use the icon component anywhere in your app without having to import it into every component that needs it, be sure to register the component in your main.js
file:
// main.js
import Vue from 'vue';
import SvgIcon from '@/components/SvgIcon.vue';
Vue.component('svg-icon', SvgIcon);
// ...
Here are a few ideas for improvements, which I intentionally left out to keep this solution approachable:
If you want to quickly take these components for a spin, I’ve created a demo app based on the default vue-cli template. I hope this helps you develop an implementation that fits your app’s needs!