Creating an SVG Icon Component in Vue
Note: I have refined this approach even further and talked a bit about it in my Laracon 2020 talk. An example of the approach for Vue 3 can be found on GitHub.
When I built GiftyDuck I wanted to use something other than Font Awesome for a change, so I settled on the beautiful Streamline Icons set which has an awesome variety of icons and variations. Unfortunately, there was no Vue component version like vue-fontawesome, so I had to make my own.
When using SVGs icons in an SPA, I believe it's almost essential to place the SVG inline on the page so that they are displayed instantly, rather than requiring a separate network request.
I have been reflecting on the approach I used and would like to document it as well as some alternative approaches I quite like.
The goal
<BaseIcon name=”user” class="text-xl text-blue-500" />
Features
- The height and width is
1em
so that it's sized appropriate to surrounding text (like FontAwesome). - The color (stroke or fill depending on the icon set) is set to
currentColor
so that it's coloured appropriate to the surrounding text (also like FontAwesome). - Tailwind utilities can be used to specify color and size (and stroke width if applicable for the icon set).
In terms of getting the SVGs actually into the component, there are probably three decent approaches I can think of...
Approach 1 — Manually paste each SVG into its own Vue component that can be loaded into the BaseIcon
component
Structure
components/ - BaseIcon.vue - icons/ - HomeIcon.vue - UserIcon.vue - [...]
Icon Example
<template> <svg>...</svg></template>
The BaseIcon
component
<template> <component :is="iconComponent" role="img" class="inline-block fill-current" style="height: 1em; width: 1em;" /></template> <script>const icons = { home: require('./icons/HomeIcon.vue'), user: require('./icons/UserIcon.vue'), // ...} export default { props: { name: { type: String, required: true, validator(value) { return Object.prototype.hasOwnProperty.call(icons, value) } } }, computed: { iconComponent() { return icons[this.name] } }}</script>
Note that the icons I'm using in this example are using fill and not stroke, so I've applied the fill-current
class. If your icons are using stroke, then you'll want to use stroke-current
instead.
Pros
- Simplest approach
- No need to mess around with webpack and loaders
- Doesn't interfere with any existing loaders applied to SVG files
Cons
- Manual process required to create the Vue component for each icon
- The files can only be used within Vue, and not in other places like PDFs, emails, etc.
Optionally import the icon components automatically
Instead of manually populating the icons
object, you could loop over the icons on the filesystem and populate it automatically.
import kebabCase from 'lodash/kebabCase' const icons = {} const requireComponents = require.context('./icons/', false, /[\w]+Icon\.vue$/) requireComponents.keys().forEach(fileName => { const iconName = kebabCase(fileName.replace(/^\.\/(.+)Icon\.vue/, '$1')) const componentConfig = requireComponents(fileName) icons[iconName] = componentConfig.default || componentConfig})
Approach 2 — Store pure SVG files in the repo and automatically create Vue components from them using vue-svg-loader
vue-svg-loader
is a handy webpack loader that will allow you to import an SVG and automatically convert it into a Vue component using vue-template-compiler
.
Install the plugin following the instructions in the README, making sure to remove any existing loaders for SVG files.
Structure
components/ - BaseIcon.vueassets/ - icons/ - icon-home.svg - icon-user.svg - [...]
The BaseIcon
component
<template> <component :is="iconComponent" role="img" class="inline-block fill-current" style="height: 1em; width: 1em;" /></template> <script>const icons = { home: require('../assets/icons/icon-home.vue'), user: require('../assets/icons/icon-user.vue'), // ...} export default { props: { name: { type: String, required: true, validator(value) { return Object.prototype.hasOwnProperty.call(icons, value) } } }, computed: { iconComponent() { return icons[this.name] } }}</script>
This component is almost identical to the first approach, with the only difference being that we're able to directly require the .svg
files rather than needing to manually wrap them in their own component.
Pros
- Pure SVG files are maintained in the repo and can be used outside of Vue context (e.g. on static HTML pages, PDFs, emails, etc.)
Cons
- Webpack configuration is required for Vue CLI (and Storybook if you're using it) to remove the existing loader for
.svg
files, before then configuringvue-svg-loader
to handle them instead. This is covered in their README. - SVG images will no longer be handled by
file-loader
so using them with<img src="...">
will not work. -
vue-svg-loader
does not appear to be very widely used. But it is a pretty simple plugin that uses the officialvue-template-compiler
under the hood so I don't see this as a big deal.
Optionally import the icon components automatically
As with the first approach, we also have the option of automatically populating the icons
object by looping over the icons on the filesystem. The only difference is where the icons are stored and how we determine the icon name from the file name.
const icons = {} const requireComponents = require.context('../assets/icons/', false, /.svg$/) requireComponents.keys().forEach(fileName => { const iconName = fileName.replace(/^\.\/icon-(.+)\.svg$/, '$1') const componentConfig = requireComponents(fileName) icons[iconName] = componentConfig.default || componentConfig})
You can see a full example of this approach at https://github.com/jessarcher/icon-component-vue-svg-loader
Approach 3 — Store pure SVG files in the repo and pull the contents of the SVG into the component using html-loader
and the v-html
directive.
Instead of creating a Vue component for each SVG, we could instead just load the SVG contents into the page using Vue's v-html
directive and the html-loader
for webpack.
Webpack config (vue.config.js
)
module.exports = { chainWebpack: (config) => { const svgRule = config.module.rule('svg'); svgRule.uses.clear(); svgRule .use('html-loader') .loader('html-loader') },};
If you're not using Vue CLI then you might need to do some different webpack sourcery (sic). Basically you need to remove any existing loaders for SVG files and use html-loader
instead. See here for an example for Storybook.
The BaseIcon
component
<template> <div v-html="require(`../assets/icons/icon-${name}.svg`)" class="inline-block fill-current" style="height: 1em; width: 1em; vertical-align: -0.125em;" /></template> <script>export default { props: { name: { type: String, required: true, } }}</script>
Pros
- As above, the raw SVGs are available in the repo for use in other contexts.
- The contents of the SVG are inserted as plain HTML rather than being converted into a Vue template, which probably removes some unnecessary overhead from the other approaches.
- There is no need to import each icon
Cons
- Similar webpack configuration grossness required as approach 2.
- As with approach 2, SVG images will no longer be handled by
file-loader
so using them with<img src="...">
will not work. - A wrapper element (e.g.
<div>
) is required to house the HTML.
You can see a full example of this approach at https://github.com/jessarcher/icon-component-v-html
Other considerations
- As mentioned above, you will need to choose between
stroke-current
andfill-current
depending on your icon set. - If you're using stroke-based icons then you can take advantage of Tailwind 1.2's new stroke width utilities.
- No matter which approach you use, the SVG icons you use may need some tweaking so that the size and colors can be customised using Tailwind. For example, you may need to remove
height
,width
,fill
, andstroke
attributes, either manually or programmatically.
Conclusion
I don't think any one approach is better than the others, as they all have different trade-offs and might work better in different scenarios. For GiftyDuck I used the first approach, however at the time of writing I'm leaning towards the second approach.
Also check out Caleb Porzio's article on the same topic with some extra tips for handling widths and heights on SVG files.