In this post we'll go over a technique to combine multiple SVG files into a single one. In the end we'll create a Phoenix Viewer Helper in order to use those icons out of that combined file.
Scalable Vector Graphics
SVG is a great format for logo and icons due to its flexibility, so it's guarantee that we have to serve a bunch of SVGs from the apps that we build. I've seen developers, including myself, serving those tiny files all individually, which works, but it's not ideal. That causes a bunch of small requests to the server and depending on the internet speed the user might feel like icons showing up one by one.
After long years of doing that I found out, by pairing with a co-worker, that we can apply a similar technique as CSS Sprites but for SVGs. This is a very simple approach and yet many developers just do not know about that, so hopefully it's going to be useful for you as it was for me.
Nesting SVGs
For this post I'll use combine the Hashrocket logo that I got from our website and a home svg icon from Google Fonts, so for now let's download some SVG icons:
hashrocket
:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.032 94.074">
<path fill="#af1f23" d="M16.708 0L0 20.143l14.677 8.13L1.003 45.274l16.425 9.524-5.251 27.179 26.093-6.206 9.674 16.445 17.021-13.943 8.78 15.801 20.287-16.876V.001L16.708 0z"/>
<path d="M42.857 30.537L26.315 47.079l6.115 6.115 16.542-16.542zm-6.219-14.873v8.578h33.121v33.126h8.615V15.676zm3.763 44.832l6.122 6.122 16.542-16.542-6.122-6.122z" fill="#fff"/>
</svg>
home
:
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
<path d="M8 42V18L24.1 6 40 18v24H28.3V27.75h-8.65V42Zm3-3h5.65V24.75H31.3V39H37V19.5L24.1 9.75 11 19.5Zm13-14.65Z"/>
</svg>
The first thing to do is to combine those into a single file by nesting those on another <svg>
tag:
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.032 94.074">
<path fill="#af1f23" d="M16.708 0L0 20.143l14.677 8.13L1.003 45.274l16.425 9.524-5.251 27.179 26.093-6.206 9.674 16.445 17.021-13.943 8.78 15.801 20.287-16.876V.001L16.708 0z"/>
<path d="M42.857 30.537L26.315 47.079l6.115 6.115 16.542-16.542zm-6.219-14.873v8.578h33.121v33.126h8.615V15.676zm3.763 44.832l6.122 6.122 16.542-16.542-6.122-6.122z" fill="#fff"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48">
<path d="M8 42V18L24.1 6 40 18v24H28.3V27.75h-8.65V42Zm3-3h5.65V24.75H31.3V39H37V19.5L24.1 9.75 11 19.5Zm13-14.65Z"/>
</svg>
</svg>
Then we can manupulate internal svg
tags this way:
- remove the internal
xmlns
attribute; - add an
id
attribute so it can be referenced later; - set a viewBox, so
height="48" width="48"
becameviewBox="0 0 48 48"
; - remove
height
andwidth
as we want to scale them; - possibly add a
stroke
andfill
withcurrentColor
;
Here's the final SVG file:
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg id="hashrocket" viewBox="0 0 94.032 94.074">
<path fill="#af1f23" d="M16.708 0L0 20.143l14.677 8.13L1.003 45.274l16.425 9.524-5.251 27.179 26.093-6.206 9.674 16.445 17.021-13.943 8.78 15.801 20.287-16.876V.001L16.708 0z"/>
<path d="M42.857 30.537L26.315 47.079l6.115 6.115 16.542-16.542zm-6.219-14.873v8.578h33.121v33.126h8.615V15.676zm3.763 44.832l6.122 6.122 16.542-16.542-6.122-6.122z" fill="#fff"/>
</svg>
<svg id="home" viewBox="0 0 48 48" stroke="currentColor" fill="currentColor">
<path d="M8 42V18L24.1 6 40 18v24H28.3V27.75h-8.65V42Zm3-3h5.65V24.75H31.3V39H37V19.5L24.1 9.75 11 19.5Zm13-14.65Z"/>
</svg>
</svg>
And that looks a bit ugly initially:
Some styling notes
There's a bunch of different ways to style those svgs, and by styling I mean to change size and color.
For sizes I imagine that a website should have maybe about 3 different sizes you could pick for an icon, so let's say small
, medium
, large
. So we'll end up generating svgs with a class that represents that sizes so we can style with css, so for now let's keep this in mind:
.icon-small {
height: 1.8rem;
width: 1.8rem;
}
.icon-medium {
height: 2.4rem;
width: 2.4rem;
}
.icon-large {
height: 3.2rem;
width: 3.2rem;
}
For colors we can use css to change a single color on a svg. Basically we'll use the css color
to set the svg currentColor
. For the home
icon it's fine, it's just a single color, so we changed the fill
and stroke
attrs to currentColor
and that's it. And for the hashrocket
icon as this is a logo and we'll never change the color we also don't need to make that flexible to change via css, so let's just keep it hard coded into the svg file. In other words we could have a link with a nested icon and the icon color could be inherited from the link styles like that:
.link { color: gray; }
.link:hover { color: blue; }
Ok so we are done with the SVG file that we are going to serve, so now let's take a look into how we're going to use our icons.
Using the Nested SVG
With the previous SVG file, let's say that we can serve that under /images/icons.svg
. The first to note is that if you open that file on browser it will show as a mess, all images will be on the top of each other. The trick here is that everytime we want to use one of those images we should build another SVG with this format:
<svg>
<use href="/images/icons.svg#search"></use>
</svg>
This way we can tell the brower which internal SVG we want to render by matching their id
attribute with the #
anchor from the url.
Making it Accessible
As a bonus section here let's make our icon accessible:
<svg aria-labelledby="title" class="icon icon-hashrocket">
<title lang="en">Hashrocket</title>
<use href="/images/icons.svg#hashrocket"></use>
</svg>
Finally it will look like this:
Phoenix helper
Finally let's build a phoenix helper to wrap up the icon usage into a function. I want to have a helper function so I could call it from my EEX templates:
<%= icon("hashrocket", :large, "Hashrocket") %>
A final note is that I want to leverage the compilation aspect of Elixir and Phoenix templates to let me know if I am trying to use an icon that no longer exists, or to help me catch typo errors. This way we want to read the SVG icons file, then extract all the ids of that svg and create a guard clause with that. So here's the help we come up:
defmodule TilexWeb.Icon do
use Phoenix.HTML
import TilexWeb.Router.Helpers, only: [static_path: 2]
@icons_path "images/icons.svg"
@external_resource "priv/static/#{@icons_path}"
@icons_svg_content File.read!(@external_resource)
@names ~r/id="([\w-]+)"/ |> Regex.scan(@icons_svg_content) |> Enum.map(&List.last/1)
@sizes [:small, :medium, :large]
def icon(name, size \\ :medium, title \\ nil) when name in @names and size in @sizes do
content_tag(:svg, class: "icon icon-#{name} icon-#{size}", aria_labelledby: "title") do
[
content_tag(:title, title || name, lang: "en"),
content_tag(:use, "", href: icon_path(name))
]
end
end
defp icon_path(name), do: static_path(TilexWeb.Endpoint, "/#{@icons_path}") <> "##{name}"
end
We are using the magic @external_resource
so if we change the icons.svg
file this icon.ex
file is re-compiled. We are also using guards for the name and the size arguments for better typo checking. Similarly you can change this helper to make it as a HEEX component very easily if your app uses phoenix live-view.
Conclusion
The idea of serving all small icons, and possibly logos and other small images out of a single image seems good to me in most cases. The final combined file will very likely be small enough, it can be compacted and that will be easily cached by the browser. And as an Elixir developer I really like the idea of having guards to tell us very early in the process of calling an icon with a typo. It's great that the language is very flexible in terms of type but also it can be more restrict when we think that we're going to benefit the most.
As an alternative I have to mention that you can also serve your icons SVGs embeded into the CSS as background images, or even other approaches, which is also a good alternative. I tend to think that styling icons like we did in this post is a bit simpler than I've seen in some projects with background images. In general both approaches are good and we should choose the approach that fits better the project.
Photo by Harpal Singh on Unsplash