"Icons - Do It All" | Inline SVG


So - icons, while being a great addition to user experience, can be causing some concerns on the development side. And, as usual, it all depends...

In the case I'm going to talk about, I wanted to:

  • only store icons I need
  • load icons on demand
  • scale icons
  • change the icon's color
  • access icons from within back-end (Ruby on Rails) and front-end (VueJS)
  • integrate icons with Quasar components (VueJS based framework)
  • have the unified approach of using icons from popular libraries as well as our custom icons.

As you may guess, this writing is somewhat specific regarding the stack we use, but the idea behind this is not bonded to it.

Font or SVG

Although Quasar's defaults are to use font icons, I prefer using SVG icons instead. It allows me to work with vendor icons the same way I would with our custom icons made in SVG.

I like to think of an icon as a standalone object rather than a part of a bigger package. Having icons in the form of separate SVG files makes it easy to load one only when needed, thus reducing an impact on a page load.

Another +1 for SVG here is that it is XML-based, which is more familiar to me than the world of fonts. And SVG enables the ability for me to manage icons with tools I already use in the project.

To me, SVG seems the closest one to "do it all" candidate in this case. So let's try to tackle our needs with it.

Changing size and color

Since SVG is defined in markup, which browsers understand, it would be nice to control the icon's size and color in the same way as it is with font icons - by just applying a CSS rule.

To make it possible, we will leverage a few attributes on the <svg> and <path> elements of the icon's markup. Here is a generic CSS class that I've tested with Material Icons, MDI5 SVG Icons, Eva Icons.

/* scss */

.svg-icon-container {
  display: inline-block;

  & > svg {
    height: inherit !important;
    width: inherit !important;
    display: inline-block;
    fill: currentColor !important;

    path:not([fill="none"]) {
      fill: currentColor !important;
    }
  }
}

We are going to apply this CSS class to the container of an SVG markup. To control the icon from the outside, we made it inherit the width and height from the container. For color inheritance, we used the fill attribute with the currentColor value.

Those rules were set in conjunction with the !important to grant complete control over attributes to the container. Now we can control the icon's color and size with CSS by applying rules to its parent. The following is an example with the SVG markup of the "star" icon from Material Icons.

<div id="star-svg-container" class="svg-icon-container" style="width: 50px; height: 50px; color: green;">
  <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
    <g><path d="M0,0h24v24H0V0z" fill="none"/><path d="M0,0h24v24H0V0z" fill="none"/></g>
    <g><path d="M12,17.27L18.18,21l-1.64-7.03L22,9.24l-7.19-0.61L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21L12,17.27z"/></g>
  </svg>
</div>

The small demo below uses the icon's container to change colors like this:

document.getElementById('star-svg-container').style.color = newColor

SVG markup may differ slightly depending on a vendor, so my CSS example might require tweaks to work with the vendor of your choice. Or, as an alternative, you can directly edit the SVG markup to fit your project specs.

In the given project, HTML rendering happens on both server and client sides. Considering that loading an icon by URL isn't an option now, we need ways to integrate the icon's markup into those rendering processes.

Using with Ruby on Rails

In Rails, an SVG markup can be treated as a regular view partial and located somewhere under the app/views/ directory.

In my case, I placed icons under app/views/shared/inline_svg directory and named files following the Rails convention for partials - with the leading underscore, like this - _star.svg.

With that done, we can now easily embed an icon into our HTML using the render helper.

<%= render "shared/inline_svg/star.svg" %>

Using with VueJS

Delivering an SVG markup into a JavaScript context requires a bit more effort.

Usually, after importing an SVG with Webpack, you get the URL, but we need to access it as raw text. Luckily, there is a plugin for that up to Webpack 4 and out-of-the-box support starting from Webpack 5.

To get our SVG as text - install raw-loader, and for such a straightforward case, we can use the plugin inline like this:

import Star from "!!raw-loader!inline_svg/_star.svg";

For convenience, I configured Webpack to resolve inline_svg to my app/views/shared/inline_svg directory.

One way to render it is to use Vue's v-html directive. This directive will place our SVG markup as innerHTML value of the element it's applied to. However, this does not go well with Quasar's q-icon component.

Quasar makes it possible to integrate an inlined SVG with its q-icon component but with one caveat - for the q-icon's size property to have an effect, the inline SVG has to be its direct child. So, we cannot just apply v-html="starSvg" to some div and pass it to the q-icon.

Well, we can apply the v-html directly to the q-icon, and it even works, but interfering in the way the component manages its children isn't a viable idea.

The other way around this, avoiding a needless wrapper, is to turn the SVG itself into a Vue component. All we need for this is to use our SVG markup as a the component's template like this:

// inline_svg.js

import Star from "!!raw-loader!inline_svg/_star.svg";

const StarSvg = { name: "star-svg", template: Star }

export { StarSvg }

Due to the frequent usage of icons, it's worth registering our icon components globally.

import { StarSvg } from "inline_svg"

// Vue 2
Vue.component(StarSvg.name, StarSvg);

// or

// Vue 3
app.component(StarSvg.name, StarSvg);

It solves our issue with Quasar integration, as now we can pass the star-svg component to the q-icon, which results in the SVG being rendered as the q-icon's direct child.

<q-icon class="svg-icon-container" size="lg" color="primary">
  <star-svg></star-svg>
</q-icon>

If needed, we may even take it further and extend our component-icons with some behavior.

Summary

Although it is a bit tedious to care about each icon like that, it provides a good amount of flexibility. We are now in complete control of what icons are loaded and when. By relying on essentials like CSS and DOM, it becomes more natural to manage the view of an icon and makes cross-environment applying easier.

Aug 28, 2021