Complete guide to SVG sprites

Creating and using SVG icon sprites in front end projects

Hajime Yamasaki Vukelic
17 min readJan 12, 2023

There are two common ways to add icons to your page. The older — and still viable — method is to use a web font. Another way is to use SVG.

Although SVG can be placed inside your HTML markup directly, it bloats the markup with unnecessary code, and isn’t very efficient if you have the same icon repeating multiple times. You could also use an <img> tag for SVG icons, but you lose much of the control over its appearance.

An alternative way to use SVG is via SVG sprites. In this article, I will show you how to export SVG icons, and how to make a single SVG sprite file out of them. I will also show you how they are used in the document, and several neat tricks that lets you control the appearance and required bandwidth.

Some sections contain information pertaining to design tools. Even if you are not a designer, it may be useful to know this information so that you can inform the designer when the output files are incorrect.

Web fonts vs SVG

SVG has the distinct advantage of offering more control over the size and appearance of the icons in different situations. It also allows us to use multi-color icons, which web fonts don’t. Another major advantage is that SVG can be manipulated with a simple text editor, while fonts require specialized tools.

Web font’s only advantage is that it requires a bit less markup in the HTML, though it requires a lot more CSS. It’s a difference between:

<span class="icon-folder"></span>

and

<svg class="icon"><use href="img/icons.svg#folder"></svg>

If you are working with lots of icons everywhere, the icon font will save you a bit of bandwidth.

Some people cite differences in file size between web fonts and SVG, but I have personally not observed this. YMMV.

Creating icons

We’ll cover the creation of icons first. Preparing a well organized set of graphics is the first step towards success.

Use artboards

Modern vector drawing tools (including Figma) will have some kind of artboard that lets the designer define separate areas for various bits and pieces of the design. It’s important to make all icons use the same-size artboard. This is because, when exported, the coordinates of the different elements in the icon are expressed relative to the artboard.

Working with artboards allows us to create multiple icons in a single document, which also makes it easier to synchronize design elements such as colors and stroke widths between different icons. Some design tools will also let you export multiple icons at once.

Consistent fill and stroke color

Using SVG gives us tremendous amount of control over colors, stroke sizes, and other properties of the graphics when they are used in the page. Consistency in defining stoke widths, colors, and other elements is quite important.

If color fills are used in the design, they should use a consistent color scheme. A good way to ensure color consistency is to use document palettes, color presets, and similar tools. This also makes modifying the graphics easier down the road (e.g., change the accent color for all icons).

Consistent stroke width

All strokes should be the same width. If multiple stroke widths are required for different parts (not recommended), the number of different widths should be limited and defined beforehand. It is certainly undesirable to use arbitrary stroke widths on a whim.

Naming layers and artboards

Artboards and layers can be given names. During SVG export, these names may be used by the program to give SVG tags some identifiers. These identifiers are used by the programmer to select the icons, so special attention should be paid to them.

In general, a naming scheme should be established in a collaboration between the designers and developers before the project starts. A good rule of thumb is to use names that do not include any spaces, either using dashes or underscore instead, or capitalizing words. Here are a few examples of developer-friendly names:

  • new-folder
  • new_folder
  • newFolder
  • NewFolder

Also make sure names start with a letter and not a number. Avoid using special characters other than a minus sign - or underscore _.

Some software will convert offending characters automatically. This behavior should be tested early on and discussed with the developers. I recommend not relying on automatic conversion as it makes communication easier. When developers and designers use the same names for the icons, it becomes quicker to identify them for troubleshooting.

We also need to make sure that layers that are not icon artboards should not have a name, as having unnecessary names can make unnecessary design elements selectable and potentially mess up our sprite, or make us work a lot harder than we need to to clean these identifiers up.

Keep strokes as strokes

If you’ve ever dealt with web fonts, you know what strokes must be expanded (converted to filled outlines). This is not necessary with SVG, and in fact, undesirable. Strokes can, and should, be kept as strokes.

Exporting icon SVGs

Most graphic design tools will have various options for exporting SVG, and a wrong option can sometimes make life difficult. This section will cover tool-specific options that help us avoid those issues.

Figma

Figma hasn’t got too many options. The important thing to remember is that whole artboards should be exported, and not just the graphics within them. We also want to ensure that the artboard has no fill.

Illustrator

In Adobe Illustrator, there are two ways to export your SVG. The “Export As” option is a bit simpler, but doesn’t let you easily control which artboards are exported and which aren’t. The new “Export for Screen” option will give you a bit more control. They have the same options for exporting SVG, though.

When using the “Export As” option, make sure the “Use Artboards” option is checked.

The export options are specified once you select the folder and file name.

The “Export for Screen” feature will let you visually select the artboards you would like to export.

We need to make sure that the format for each artboard is “SVG”.

The export options are selected using the cog icon next to the “iOS” and “Android” buttons.

Regardless of the export method you use, the options relevant to the SVG output are the same.

The “Inline style” option should be selected. This makes the styling information be output as style attributes. This is important for SVG sprites. (You can technically use either of the two options for this other than “Internal CSS”.)

The “Object IDs” option should be set to “Layer Names”. As discussed before, the layer names are used as the id attribute on relevant SVG tags.

Other options may be left as they are.

Affinity Designer

To export SVG in Affinity Designer, we can use either the Export persona or the “Export” option under the “File” menu. The options are the same. Export persona is probably better as it allows us to export multiple icons at once.

When exporting using the “File” menu, we need to be sure the individual artboard is selected in the “Area” drop-down.

In the Export persona, selecting the artboard will give us some export options in the “Export Options” panel.

The file format should be “SVG”. An important option is the export resolution. It should be set to “Use document resolution”.

Additionally the following options should be turned on:

  • “Relative coordinates”
  • “Use hex colors”
  • “Flatten transforms”
  • “Set viewbox”

The “Add line breaks” option will make extracting the relevant parts of the output simpler so I recommend keeping it on.

In the Export persona, options can be set for multiple artboards at once having them all selected while changing the options.

The sprite file

Once we have the exported SVG, we can start preparing the sprite SVG file.

An empty SVG sprite file looks like this:

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- icons go here -->
</defs>
</svg>

The <defs> tag will house a number of <symbol> tags. The <defs> tags contains a library of graphics for later use — which is what icon sprite is— and isn’t drawn when the SVG file is opened in a viewer or used directly on the page.

Notice that the <svg> tag does not have a viewBox attribute. You may have seen it used in some other examples, but it is not necessary for icon sprites and should be omitted.

Defining symbols

Now it’s time to start defining symbols for individual icons.

Each icon is housed within a <symbol> tag. Typically, it looks like this:

<symbol id="my-icon" viewBox="0 0 24 24">
<!-- icon content -->
</symbol>

The id is the icon’s identifier. This will be used to reference it from our HTML document, so it’s quite important.

When SVG is drawn on the page, it receives its own viewport whose physical dimensions on the screen are defined by the CSS and the page layout. The viewBox attribute defines the size and location of the graphics within the that viewport. This attribute has four numbers. The first two numbers represent the x and y coordinates of the top-left corner of the graphic. The other two numbers represent the width and the height respectively. When the browser renders a graphic, it will translate it by the x and y amounts and render only the parts that fit within the width and height. It will then map the rendered image to the physical viewport within the page by proportionately scaled it to completely fit the viewport. If the viewport is larger than the graphic, the graphic is scaled up, and vice versa.

In concrete terms, let’s say we have a 32px square icon with 2px stroke width. If the SVG’s viewport is also a 32px square, the icon is drawn without any scaling. If the viewport is twice as large, the icon will be drawn at 64px square and the stroke will be 4px wide. If the viewport is 16px square, then then icon is also drawn as 16px wide, and the stroke is only 1px wide.

But what happens when there is no viewBox on the <symbol>? When the viewBox attribute is not specified, the browser will treat the SVG as if the viewbox matches the viewport. In that case, the 32px icon is always drawn as 32px regardless of the viewport size. If the SVG viewport size is 64px square, the icon is drawn in the top-left corner at 32px square. If the viewport is 16px square, only the top-left quarter of the icon is drawn within the viewport.

All this is to say that you should always include the correct viewBox on the <symbol> tag.

Converting the source SVGs

Since the output of various design tools is different, we’ll discuss conversion by looking at concrete examples.

Converting Figma SVG

Here’s the example output from Figma.

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="folder-add">
<path d="M9.5 8V5.5H19.5V18.5H4.5V8.5H9C9.27614 8.5 9.5 8.27614 9.5 8Z" fill="#E7CA62" stroke="black" stroke-linejoin="round"/>
<path d="M15 12V16M13 14H17" stroke="black" stroke-linecap="round"/>
</g>
</svg>

The converted symbol looks like this:

<symbol id="folder-add" viewBox="0 0 24 24">
<path d="M9 8.5C9.27614 8.5 9.5 8.27614 9.5 8V5.5H19.5V18.5H4.5V8.5H9Z" fill="#E7CA62" stroke="black" stroke-linejoin="round"/>
<path d="M15 12V16M13 14H17" stroke="black" stroke-linecap="round"/>
</symbol>

We copy the viewBox attribute to the <g> tag and remove the outer <svg> tags. We rename the <g> tag to <symbol> and keep everything inside it as is.

Converting Illustrator SVG

Here’s the example output from Illustrator:

<?xml version="1.0" encoding="UTF-8"?>
<svg id="folder-add" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="m9.5,8v-2.5h10v13H4.5v-10h4.5c.28,0,.5-.22.5-.5Z" style="fill: #e7ca62; stroke: #000; stroke-linejoin: round;"/>
<path d="m15,12v4m-2-2h4" style="fill: none; stroke: #000; stroke-linecap: round;"/>
</svg>

The converted symbol looks like this:

<symbol id="folder-add" viewBox="0 0 24 24">
<path d="m9.5,8v-2.5h10v13H4.5v-10h4.5c.28,0,.5-.22.5-.5Z" style="fill: #e7ca62; stroke: #000; stroke-linejoin: round;"/>
<path d="m15,12v4m-2-2h4" style="fill: none; stroke: #000; stroke-linecap: round;"/>
</symbol>

We rename the <svg> tag to <symbol> and we keep the id and viewBox attributes removing the rest.

Converting the Affinity Designer SVG

Here’s the example output from Affinity Designer:

<?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 24 24" 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-linejoin:round;">
<rect id="folder-add" x="0" y="0" width="24" height="24" style="fill:none;"/>
<g id="folder-add1" serif:id="folder-add">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
<path d="M15,12l0,4m-2,-2l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</g>
</svg>

The converted symbol looks like this:

<symbol id="folder-add" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
<path d="M15,12l0,4m-2,-2l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>

We extract the <g> tag and its contents from the file, and copy the viewBox attribute from the <svg> tag. We also remove the serif:id attribute which is only used by Affinity Designer.

Note that Affinity Designer suffixes the id with a number (in this case it’s exported as folder-add1 instead of folder-add). We removed this number as it’s not needed.

Adding icons to our document

Now that we have our sprite file, we can start adding icons to the document. To do this, we use an inline <svg> tag which contains a single <use/> tag. Supposing the icon sprite is saved in img/icons.svg, it would look something like this:

<svg><use href="img/icons.svg#folder-add" /></svg>

SVG is XML, so you do need / on self-closing tags like <use/>. (In HTML5, these are allowed by the browser but not needed.)

The URL contains a fragment identifier, #folder-add, which matches the id of a symbol in the sprite SVG.

By default, due to the viewBox attribute we included in our <symbol> tags, the icon will render at the size defined by it. We can specify the rendered size of the icon via CSS. To do this, we can add a class attribute on the icon <svg> on our page and select that.

<svg class="icon"><use href="img/icons.svg#folder-add" /></svg>

Then we can define the icon size as follows:

.icon {
width: 1.5em;
height: 1.5em;
}

Since the original design is 24×24px, we use 1.5em for the icon size. At the default browser font size of 16px, it makes the icons 24px. It also allows the icons to scale with the surrounding text, making it unnecessary to define multiple sizes like icon-large and similar.

Customizing icon colors

Depending on the context, the same icon may appear in different colors. We may also need to support different color schemes for dark mode, or simply be asked to change the color scheme completely due to art direction decisions. While we can include different color variants in the sprite itself, it’s more efficient to include a single variant and control the color via CSS. This can be done in one of two ways.

Before we go further, I will make a small digression and remind you about the two icon types that designers typically use. Icons can either be line art or normal icons. Line art uses only strokes, while the normal icons use a combination for fill and stroke.

Line art (left) vs normal icon (right)

For line art icons, the icons comprise solely of strokes. While converting the icon SVG to symbols, we can simply replace the stroke color with the currentColor keyword (it can also be spelled all-lower-case, currentcolor). This is a magic value that represents the (parent) element’s text color (the CSS color property). By doing this, the stroke will always have the same color as the surrounding text, and we can change the color for individual icons by using the color property where needed. For instance:

.alert .icon {
color: red;
}

For normal icons, we can use CSS custom properties (CSS variables) in the SVG itself. Let’s take a look at this on a concrete example:

<symbol id="folder-add" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z"
style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
<path d="M15,12l0,4m-2,-2l4,0"
style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>

The above SVG represents the following icon:

This icon has two colors: the black stroke and the yellow fill. We will define two CSS variables for these in our CSS.

:root {
--icon-fill: #e7ca62;
--icon-stroke: black;
}

We can also use the currentColor keyword in some of the properties in the normal icons, too.

:root {
--icon-fill: #e7ca62;
--icon-stroke: currentColor;
}

In the SVG, we make the necessary adjustments to take advantage of these custom properties.

<symbol id="folder-add" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z"
style="fill:var(--icon-fill);fill-rule:nonzero;stroke:var(--icon-stroke);stroke-width:1px;"/>
<path d="M15,12l0,4m-2,-2l4,0"
style="fill:none;fill-rule:nonzero;stroke:var(--icon-stroke);stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>

Later, we customize the icon colors like this:

.alert .icon {
--icon-fill: red;
}

Note that all of this applies to gradients as well. We just treat them as multi-color icons.

Swapping the values in the SVG files can be done more efficiently after they’ve been converted to the sprite. Doing a few find-and-replace operations across the entire sprite file makes this a relatively painless job.

Defining the stroke appearance

The stroke in SVG has several characteristics that define its appearance.

Some of these characteristics are baked into the individual graphics via attributes, but sometimes they are not. For instance, if we go back to our Affinity Designer example, we will notice that this bit of code is defined on the <svg> element rather than the graphical elements:

<svg ... style="... stroke-linejoin:round;">

Since we’re not keeping the <svg> tag, if we don’t define these somewhere, the graphics will look wrong. The simplest way to deal with this discrepancy is to do it in the CSS.

.icon {
stroke-linejoin: round;
}

We can generally remove these properties completely from the individual SVG symbols even if we are not working with Affinity Designer files, and move them to CSS in order to save some bandwidth and gain more control over them. While doing so, we should be mindful of the situations where the designer deliberately used different characteristics for the different parts in their graphics.

Customizing stroke widths

As discussed before, SVG is scaled proportionately when the rendered viewport scales. This includes the stroke width. Depending on the way the design was intended to work, this may or may not be desirable. For example, the designer may wish for the stroke width to remain constant regardless of the icon size. In that cases, we want full control over the stroke width in CSS. We can achieve this in one of two ways.

The first way is to simply strip out all stroke width declarations in the SVG, and add a single global one in the CSS:

.icon {
stroke-width: 1px;
}

Another way is to use CSS custom properties.

I recommend using custom properties rather than hard-coded values for two reasons. Firstly, you may be dealing with more than one stroke width (not recommended, but it could happen). Secondly, a custom property defined on the parent will be apply to all children unless they also do their own overrides. This is convenient as we do not have to specifically target SVG elements to apply a different stroke width.

We can apply the custom properties either directly in the SVG or, again, by stripping out stroke-width declarations in the SVG, and applying one in CSS. Here’s how the latter option is done:

:root {
--icon-stroke-w: 1px;
}

.icon {
stroke-wdith: var(--icon-stroke-w);
}

The key thing to remember here is that the stroke width is defined in terms of the SVG coordinate system, not in terms of the page. A stroke width of 1px is only physically a 1px in CSS terms when the SVG’s own viewport size matches the viewBox dimensions. In all other cases, it is rendered differently.

Let’s suppose we’ve set up the icon size as discussed before, using em values. Now, let’s suppose the icon is located within an element whose font size is 200%. The strokes scale with the icon size, so we are now talking about strokes that are twice as wide. The designer specified that the stroke must be constant, so we need to scale it back.

.project-header {
font-size: 200%;
--icon-stroke-w: 0.5px;
}

Since the base stroke width is 1px, and it has been scaled to 200%, then we need to set the stroke width to 0.5px (half-width) so that it will still be rendered as 1px when scaled by 200% (0.5 × 2 = 1).

Reusing symbols within the sprite

Sometimes, the same graphic may repeat over and over across different icons. Consider the following example:

We have three variants of the same graphic. In virtually all design tools, the common part — the yellow folder — will repeat three times. We can save some bandwidth by making sure these do not actually repeat in the SVG sprite.

Let’s first look at the SVG sprite that hasn’t been refactored.

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="folder" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
</symbol>
<symbol id="folder-add" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
<path d="M15,12l0,4m-2,-2l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>
<symbol id="folder-remove" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
<path d="M13,14l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>
</defs>
</svg>

The <path d=”M9.5,8l0,-2.5l10,0l0,13l..."/> tag repeats three times. We can factor this out by using the <use/> tag:

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<symbol id="folder" viewBox="0 0 24 24">
<path d="M9.5,8l0,-2.5l10,0l0,13l-15,0l0,-10l4.5,0c0.276,0 0.5,-0.224 0.5,-0.5Z" style="fill:#e7ca62;fill-rule:nonzero;stroke:#000;stroke-width:1px;"/>
</symbol>
<symbol id="folder-add" viewBox="0 0 24 24">
<use href="#folder"/>
<path d="M15,12l0,4m-2,-2l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>
<symbol id="folder-remove" viewBox="0 0 24 24">
<use href="#folder"/>
<path d="M13,14l4,0" style="fill:none;fill-rule:nonzero;stroke:#000;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;"/>
</symbol>
</defs>
</svg>

Now the folder-add and folder-remove icons are using the folder icon as part of their graphic.

Remember that the <use/> tag supports x and y attributes that can be used to translate it around the viewbox. This means that we can use this refactoring even when the repeating elements are not always in the same spot within the icon.

Dealing with cross-domain issues

If you want to host the sprite file on a separate domain (e.g., CDN), you will notice that it does not work. This is due to CORS restrictions. Sadly there is no good way to work around this and keep the sprites on the CDN.

The first option is to simply not use cross-domain sprites. This is the easiest solution, but results in duplication.

The second option is to inline the SVG using JavaScript. The code to do this could be something like this:

fetch('https://cdn.my-domain.com/icons.svg')
.then(res => res.text())
.then(svgText => {
let svgHider = Object.assign(document.createElement('div'), {
innerHTML: svgText,
})
Object.assign(svgHider.style, {
position: 'absolute',
width: 0,
height: 0,
overflow: hidden,
pointerEvents: 'none',
})
document.body.append(svgHider)
})

The <use/> tags should not include the full URL but just the fragment identifier when using this method:

<svg class="icon"><use href="#folder-add"/></svg>

Remember that id's are unique for the entire document. The id reference in the <use/> tag may refer to any element on the page, not just the icons within the sprite. It is therefore recommended to prefix the id's in the sprite SVG with a common prefix like icon- to avoid conflicts with the id's used by HTML elements in the page.

The second option may sound “better” as it keeps the sprites on the CDN, but it actually isn’t because it means the sprites won’t work without JavaScript.

Automating sprite creation

If you want to automate sprite creation with this level of attention to detail, sorry, I don’t know of any tool that does it. And it does not surprise me given there isn’t a standard way in which design tools themselves output SVG.

Some of the tools may work well enough for the particular design tool that the designer is using, and may provide a good-enough starting point for your icons. You’ll just need to try them to find out.

Having said that, doing all of this manually isn’t exactly hard either. Personally, I prefer to have this level of control over what the final output looks like as this this is something I only need to do every once in a while. It does takes a bit of practice, but what doesn’t!

--

--

Hajime Yamasaki Vukelic

Helping build an inclusive and accessible web. Web developer and writer. Sometimes annoying, but mostly just looking to share knowledge.