Josh Collinsworth Josh Collinsworth is a senior frontend developer at Shopify. He's also the creator of Quina, a strategic logical word game app, and co-creator of Thomas, a small child.

Using SVG and Vue.js: A complete guide

12 min read 3443

Vue Logo

What is SVG?

Scalable Vector Graphics (SVG) is one of the most useful and versatile tools at a frontend developer’s disposal. With SVG, we can have dynamic images that scale to any size, often for a fraction of the bandwidth of traditional raster image formats such as JPEG and PNG.

But SVG can be trickier to use well, especially in modern JavaScript frameworks like Vue.js. From the simple question of how best to load SVG files into your templates, to handling animations and accessibility, there are plenty of pitfalls to avoid.

I’ve been working with Vue and SVG for the last few years, and I’ve developed (pun intended) a few tricks and recommendations I’d like to share.

While the examples in this post will all be shown with Vue, the general idea should work with any component-based framework.

Let’s dive in!

Using SVG with Vue

While there are technically more than three ways to go about the task (HTML by default has several, and that’s before we’ve even added Vue to the mix), I always find myself gravitating to one of the following three methods to mastering SVG. The vast majority of use cases are covered in the three approaches below.

1. Loading a standard HTML SVG

There are several ways to load an SVG file to the page by default in the browser, and any of them will work with Vue. At the end of the day, Vue uses dynamic HTML templating, meaning the most straightforward way to use SVG with Vue is exactly how you might do it with standard HTML:

  • By placing the <svg> inline
  • By using an <img> tag that links to an external SVG file with the src attribute

Each method has its tradeoffs, but they’re the two methods users are most familiar with, so I won’t dwell too much on these here.

Suffice to say, if it works in HTML, it will work in Vue. So, while you’re not getting anything new or flashy with this approach, it’s also straightforward.

2. Using vue-svg-loader

Let’s move into some slightly more exciting territory. There are plenty of Vue-specific methods for working with SVG files. My personal favorite is vue-svg-loader.

We made a custom demo for .
No really. Click here to check it out.

The only real drawback of vue-svg-loader is that to use it, you need to run an npm install and edit your webpack config (see the directions here). It’s not much work, but not all projects are keen on adding dependencies or additional configs. Depending on the project, you may not even be in control of those options.

If you do have that freedom, however, vue-svg-loader enables you to import SVG files and use them just like Vue components, which is both simple and convenient. After installing vue-svg-loader, we can do this:

<!-- vue-svg-loader demo -->
<template>
    <div id="app">
        <h1>Hello, world!</h1>
        <LogoSVG />
    </div>
</template>

<script>
import LogoSVG from './logo.svg';

export default {
    components: { LogoSVG },
    // ...Other stuff here
}
</script>

As mentioned, there’s some installation and setup required to go this route, so be sure to check the vue-svg-loader documentation, but it’s easy to use once you get it running.

As a bonus, the SVG file is placed directly into the DOM where it’s used, so you can target specific pieces of it with JavaScript and/or CSS for the sake of animations and styling.

There are also some other Vue-specific options for getting SVG into your app covered in this StackOverflow post, but in my experience, vue-svg-loader is the best of the bunch for most use cases.

That brings us to our third option.

3. SVG Vue components

This is my personal favorite method, and we’ll spend most of the rest of the article covering it in depth. This approach allows you to have the most flexibility with animations, as well as direct control over SVG elements. It also allows for easy imports — all with no additional dependencies or config changes!

Getting started with SVG Vue components

Any time I work with SVG in Vue, my preference is almost always to create a new Vue component that is solely for that specific SVG file’s markup and styling.

Then, wherever I want to use **that SVG in my project, I simply import and use it like any other Vue component. Here’s a basic example SVG component for demonstration purposes:

<template>
    <svg class="my-svg-component">
        <!-- Actual SVG paths, shapes, etc. here -->
    </svg>
</template>

<script>
export default {
   name: 'MySVGComponent',
}
</script>

<style scoped>
.my-svg-component {
    /* Scoped CSS here */
}
</style>

This approach has several key benefits:

  • It makes SVG files easy to use anywhere. Just import and use it like you would any other Vue component
  • The SVG file is rendered to the page inline, so its individual pieces remain available for styling, animations, etc., but it doesn’t eat up extra space in your main template files and components
  • This method includes all of the goodies you get using Vue, meaning you have access to things like props and logic. If you have alternate versions of an SVG (e.g., an inverted SVG for dark mode or multiple versions of the same icon), you can easily take advantage of Vue magic

The downside? It takes some extra work to set up Vue SVG components. You’ll have to manually move (and possibly edit) the actual markup of the SVG file. It may not be worth the trouble to bring an SVG file into a Vue component in a situation where it’s merely a static image that will never change, need logic, or be reused.

If you do find yourself wishing you could update the appearance of an image with some JavaScript logic, or make an image adaptable to the current app state, SVG Vue components are a perfect fit!

Let’s look at an example. Let’s say you have a “favorite” icon button that the user can click. Initially, it’s just an outline (shown left), but if the user clicks it, the star then fills in to indicate that the user has marked the item as a favorite (shown right):

Favorite Icon Starred

In the old days, this would mean you’d need two different images that you would swap out on the fly. But with SVG and Vue, you can easily create a single component that takes a prop instead and renders the state of the icon accordingly.

Let’s look at the code:

<template>
    <!-- Note: several SVG and path attributes omitted for brevity -->
    <svg
        :class="{ starred }"
        :title="starred ? 'Starred' : 'Unstarred'"
        role="img"
    >
        <path />
    </svg>
</template>

<script>
export default {
    name: 'FavStar',

    props: {
        starred: {
            type: Boolean,
            default: false,
        }
    }
}
</script>

<style scoped>
path {
    fill: none;
}

.starred path{
    fill: #ffd100;
}
</style>

Side note: obviously, a real SVG file would need more markup than what’s shown above (like a viewBox attribute, for starters), but I’ve omitted the bits that aren’t relevant to Vue for the sake of readability.

The starred class is only added to the SVG file if the starred prop is provided, making the component dynamic. ({ starred } is shorthand for { starred: starred }, where the first is the class to conditionally add, and the second is the Vue prop to conditionally check.)

The component above is now ready to import and drop in anywhere else in your codebase. For example, you might use it in a <button> that delegates data and handles clicks:

<button @click="starOrUnstar" :aria-pressed="isStarred">
    <FavStar :starred="isStarred" />
</button>

I won’t go so far as to flesh out that entire button component, but hopefully, it’s easy to see how the FavStar component can be imported and used wherever it’s needed, and how its appearance can be managed with Vue data.

Handling SVG accessibility in Vue

In the example above, you may notice that the title attribute is also dynamic based on the starred prop. That’s because in this case, it’s important to communicate the status of the icon non-visually.

A core tenet of accessibility (or “a11y”) is that you shouldn’t need sight to tell what state something is in or what it means; every meaningful part of the page should be perceivable non-visually.

Because our star icon has semantic meaning (it communicates to the user whether they’ve previously starred or favorited an item), we need to ensure a user relying on assistive technology such as a screen reader can tell the state of the icon — pressed or unpressed — non-visually. The title attribute handles that quite easily, thanks to a simple ternary.

In addition, because the button wrapping the SVG is effectively a two-way toggle switch — the button either stars or unstars the item it’s associated with — I also chose to use a dynamic :aria-pressed="isStarred" on the button itself.

This way, the button conveys its own current state, as well as implicitly identifying itself as a toggle. It has two possible states: on or off, true or false.

Without this, an assistive technology user could conceivably focus the button and hear their screen reader announce “starred” or “unstarred” from the SVG’s title, and be confused by a lack of further context on what the button does.

When making accessibility decisions, it’s always important to test with real users. It’s quite easy to misuse markup intended to make things more accessible (especially ARIA) and end up with the opposite effect.

You may even have the desired effect but find out it’s not at all what your users prefer. So, be sure you’re not making any assumptions about how your accessibility choices impact your users. Find out from the source instead.

To learn more, Deque has an excellent list of ways to handle accessibility on SVG, which I highly recommend reading. The primary method described is basic, but there are others, including adding a <text> element directly inside the <svg>.

Bear in mind that many SVG files will be purely decorative and their description won’t add anything to the experience.

For example: imagine a button that says, “Add another item” and features a “+” SVG icon for decoration.

Add Another Item Icon

In this case, the “+” icon adds nothing because the text already says everything that needs to be said.

Whenever an SVG file does not communicate any additional information, it’s best to hide the SVG file from assistive technology. The simplest way to do this is to add aria-hidden="true" to the <svg> element, though you can also add role="img" and alt="" together to cover your bases.

Creating dynamic icons in Vue

As long as we’re working in Vue components, we have access to all of Vue’s powerful logic and tooling. We’re not limited to just one SVG per component; we could conditionally render any number of individual elements using v-if or v-show, implement v-for loops, and even add event handlers to individual elements.

Let’s imagine a tag component with three states:

  1. Normal, with no icon
  2. With a checkmark icon
  3. With a red “x” icon

Three Tags

Here, we may not even need SVG — all of this is simple enough to render with pure CSS.

However, it’s conceivable that the icon could be helpful in other parts of our application, where we might want to show similar success/error states. So, making the icon reusable could help us out down the road.

We can create a single icon out of the checkmark and “x” circles by overlaying them on top of each other so that their individual strokes look something like this:

Check Icon

Once we’re working with a single SVG, we can export it alone and create a Vue component that takes a status prop.

Based on what status prop the component receives when it’s used (and whether it’s valid), the component displays either the checkmark <path> or the <g> that makes up the “x” and alters the background of the icon accordingly.

<template>
    <svg v-if="iconBackgroundColor" width="100%" height="100%" viewBox="0 0 58 58" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
        <circle :style="{ fill: iconBackgroundColor }" cx="28.546" cy="28.546" r="25.546" />
        <path v-if="status === 'success'" d="M16.243,28.579l8.18,8.181l16.427,-16.427" />
        <g v-if="status === 'error'">
            <path d="M20.333,37.747l16.427,-16.427" />
            <path d="M20.333,21.32l16.427,16.427" />
        </g>
    </svg>
</template>

<script>
const colors = {
    success: '#ffd100',
    error: '#e4002b',
}

export default {
    name: 'StatusIcon',

    props: {
        status: {
            type: String,
            default: '',
            validator(status) {
              return colors[status] || status === ''
            }
        }
    },

    computed: {
        iconBackgroundColor() {
            return colors[this.status]
        }
    }
}
</script>

<style scoped>
path,
circle {
    stroke: #ffffff;
    stroke-width: 6px;
    fill: none;
}
</style>

In this case, there are only two states: success and error. But it’s easy to imagine similar icons being needed, and this component can make use of at least one or two more icons and colors.

If that happens, it’s fairly straightforward to add another <path> to the SVG file and another color to the colors object. Another benefit is that this would automatically update the status prop’s validator.

It’s easy from here to imagine dynamic icon components that may contain dozens of icons or variations and the power and flexibility that it provides code authors.

You could even imagine an <Icon> component that houses multiple, different SVG files in its template, and conditionally renders the right one based on an incoming prop.

Editing SVG files and moving markup to Vue components

You may or may not be into creating vectors, but odds are pretty good that the SVG files you’re working with daily were created by a designer and exported from a design tool like Adobe Illustrator or Figma. You might not know anything about actually creating SVG files, and that’s fine!

It’s often helpful to run SVG files through an optimization engine like SVGO. (There’s also a GUI version of SVGO by Jake Archibald if you prefer GUIs to CLIs).

This should strip unnecessary things from the SVG file before you use it, making it as performant as possible. Be sure to compare the optimized version with the original carefully to be sure nothing was removed that shouldn’t have been.

Hopefully, when the SVG file was exported, the design software was properly configured to optimize it already, so this step is redundant. But you never know, and that’s generally more in the realm of concerns for developers than designers anyway.

In any case, to get the .svg file into a Vue component, open it in your text editor of choice (VS Code, Sublime Text, etc.) and copy the <svg> portion from opening tag to closing tag. (Note: this might be a lot of text, and it might be easier to view with line wrapping toggled in your editor!)

Next, simply paste the <svg> and its contents into a Vue component. It will likely need some modification. For example, here’s the originally exported SVG of my star icon from above:

<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "<http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd>"><svg width="100%" height="100%" viewBox="0 0 203 194" version="1.1" xmlns="<http://www.w3.org/2000/svg>" xmlns:xlink="<http://www.w3.org/1999/xlink>" xml:space="preserve" xmlns:serif="<http://www.serif.com/>" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><path d="M95.188,7.487c1.199,-2.297 3.575,-3.737 6.166,-3.737c2.591,0 4.967,1.44 6.165,3.737c0,0 16.769,32.138 24.044,46.08c2.079,3.986 5.907,6.767 10.34,7.513c15.508,2.61 51.254,8.627 51.254,8.627c2.555,0.43 4.659,2.245 5.46,4.709c0.8,2.464 0.165,5.169 -1.649,7.019c-0,-0 -25.383,25.878 -36.395,37.105c-3.148,3.21 -4.61,7.709 -3.95,12.156c2.31,15.556 7.634,51.412 7.634,51.412c0.38,2.563 -0.695,5.124 -2.791,6.647c-2.097,1.523 -4.866,1.755 -7.185,0.601c-0,-0 -32.456,-16.144 -46.537,-23.147c-4.025,-2.002 -8.756,-2.002 -12.781,-0c-14.08,7.003 -46.536,23.147 -46.536,23.147c-2.32,1.154 -5.089,0.922 -7.185,-0.601c-2.096,-1.523 -3.172,-4.084 -2.792,-6.647c0,-0 5.324,-35.856 7.634,-51.412c0.66,-4.447 -0.802,-8.946 -3.95,-12.156c-11.011,-11.227 -36.394,-37.105 -36.394,-37.105c-1.815,-1.85 -2.45,-4.555 -1.649,-7.019c0.8,-2.464 2.904,-4.279 5.459,-4.709c0,-0 35.747,-6.017 51.254,-8.627c4.434,-0.746 8.261,-3.527 10.341,-7.513c7.275,-13.942 24.043,-46.08 24.043,-46.08Z" style="fill:none;stroke:#ffd100;stroke-width:7.5px;"/></svg>

You might be wondering what’s with the XML and DOCTYPE tags.

They’re there for compatibility with other document types, such as XML. They don’t do anything useful in HTML. As long as your SVG file is only intended for use in HTML, everything before the opening <svg> tag can be safely removed. The same goes for the version, xmlns, and xml attributes.

The main part to look out for is CSS styling.

Depending on how the SVG file was exported, it may have inline style attributes on one or more of the SVG elements, or it may have a <style> tag with classes, which are then added to the SVG file’s elements.

If it’s the latter, don’t worry too much, as class-based styles are easy enough to override in your CSS as needed.

But if the SVG file has inline styles, you’ll want to judiciously prune the CSS so you can more easily apply the styling you want.

When working with my star icon, for example, I needed to remove fill:none; from the list of inline styles so that I could add the fill color only when desired. (The only way to override an inline style is with the !important flag, which is generally best to avoid where possible.)

You could also copy all the inline style out and put it in the Vue component’s <style> block if you wanted to, but I prefer to only do that where it makes sense and let the rest stay where it is.

If I intended to change this star icon’s outline color at any point, for example, I would move the stroke property out of the markup and down into the <style> block.

Whatever you choose to do, you now have a Vue component that houses an SVG, and you’re ready to rock with styling, logic, or whatever else you might want.

Dealing with SVG gotchas in Vue

Be aware that when bringing SVG into the browser, not everything possible with vector in design software is also possible with SVG in the browser.

For one thing, effects such as shadows and gradients can work differently in the browser than in design software. The results can be mixed, and it may be better to recreate these effects in CSS where possible.

The larger issue is that most vector drawing programs will allow you to align a stroke to the middle, inside, or outside of a path, as in the image below:

Vector Outlines

However, SVG files in the browser can only be aligned to the middle of a path, so there is no option for inner or outer alignment.

If you need the appearance of a stroke that’s aligned to the inside or outside of the path, you’ll either need to offset the middle-aligned stroke appropriately by half of its stroke width, or expand the stroke into its own vector object.

This could be challenging if you’re animating the stroke, so be sure to work closely with the SVG author in these cases. You can certainly achieve any effect you’re going for — it may just require some creative workarounds.

Conclusion

When working with SVG in Vue, we’ve looked at three popular methods:

  • Standard HTML SVG
  • vue-svg-loader
  • Vue SVG components

In most cases, Vue SVG components are my personal preference, but there are benefits and tradeoffs to each of the three approaches discussed here.

Method: Best for: Drawbacks:
Standard HTML SVG Quick and easy placement of static SVG images Depending on approach, either muddies templates with SVG markup, or prevents individual elements of SVG from being styled or targeted
vue-svg-loader Using SVG like Vue components, without the need to edit touch SVG files Requires installation and config changes
Vue SVG components Maximum control and convenience Requires manually editing SVG files, which can be daunting and/or tedious

Hopefully you’ve enjoyed the read, and gained at least a little bit of insight and appreciation for the power combo that is SVG and Vue!

Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - .

Josh Collinsworth Josh Collinsworth is a senior frontend developer at Shopify. He's also the creator of Quina, a strategic logical word game app, and co-creator of Thomas, a small child.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply