Pastanaga icon system

by Victor Fernandez de Alba - January 25, 2018

The way we deal with icons in the web has evolved over the years. Images, images sprites, fonts, SVG, SVG sprites… I’ve been looking lately for the current best practice in order to include it in Pastanaga and I wanted to share with you my results. Please note that it’s not closed and I’m open to suggestions. PRs are welcome too!

Abandon font-based systems

It was clear to me that font-based icon systems are a no longer an option today. For several reasons:

  • The font is loaded every single time on the page, regardless if we use all the icons or none of them. This bloats the application size and forces an additional request (in the best case scenario).
  • An existing font is difficult to create, maintain and update. You can use some online (free) services to do that and even you can forge your custom icon font with them but it’s cumbersome and not practical.
  • Forces you to maintain a parallel CSS that maps the icon name with its actual character in the font (which is obscure). The font creation tool helps you with that, but…
  • Extending them with new icons is also complex, especially for newbies, and you need access to the source and reload the source in the same tool that was created.

only to name a few.

Time to move on: inlining SVG

The rise of SVG in modern web developing is for a good reason:

  • SVG is a vector format, so it looks great in HiDPI displays
  • It’s lightweight and portable, since it’s not a binary file
  • It can be styled (and animated) easily using CSS (provided they are inlined, not used with the <img /> tag
  • You can control it via JS

My initial feeling was that using a SVG sprite based system would be the best approach, but I soon was discouraged, after reading to Chris Coyier, CSSTricks: A Pretty Good SVG Icon System

All in to inlining SVGs, then.

So, we need an SVG icon system. Luckily for us, Pastanaga already has a complete set of icons based on SVG organized in one file per icon.

Goals

Our main goal is to provide inline SVGs in our applications, having in mind:

  • It should be performant and small in size
  • Only the used icons should be loaded in the given view, compatible with lazy loading
  • Has to be a no-brainer and clutter-less from the developer point of view
  • You should be able to extend (or override) the default icon set with your own icons easily
  • Valid for all modern frameworks, with focus on Angular and React

Harnessing the power of Webpack and modern JS

As developers we want to use the tooling that we have at our hands in the best possible way. So our icon system should use simple ES6/7/* and TypeScript conventions.

import myIcon from './icons/my-nice-icon.svg';
import Icon from './components/Icon';

and the from JSX:

<Icon name={myIcon} />

or angular template:

<Icon [name]="myIcon"></icon>

or

<div icon [name]="myIcon"></div>

Deconstructing the SVG and put it back together again

According to all the use cases shown in this interesting article by Amelia Bellamy-Royds in CSSTricks: How to Scale SVG the most sensible approach when inlining SVGs is to simply just set the viewBox on your <svg> tag, and set one of height or width to auto. The browser will adjust it so that the overall aspect ratio matches the viewBox. As Amelia points out, that would work for all modern browsers back until 2014. If we have to support older ones, we will need to apply for those the famous padding-bottom hack. Let’s keep things simple for now.

Let’s assume that our SVG is not perfect, and we want to have the all the flexibility that a modern browser can achieve handling SVGs. We will take the existing SVG, deconstruct it and get all the SVG attributes, then the content. We will then put it all together in our components, exactly the way we want it.

The Webpack part

We can accomplish all our goals by using a Webpack loaders combo for loading SVG:

{
    test: /\.svg$/,
    include: path.join(paths.appSrc, 'icons'),
    use: [
        {
        loader: 'svg-loader',
        },
        {
        loader: 'svgo-loader',
        options: {
            plugins: [
            { removeTitle: true },
            { convertPathData: false },
            { removeUselessStrokeAndFill: true },
            { removeViewBox: false },
            ],
        },
        },
    ],
},

We will use svg-loader a super simple inline svg loader that provides you extra flexibility when handling your SVG. Initially I tried the popular Webpack Team’s svg-inline-loader but it was not that flexible at the end. svg-loader returns an object with the contents and the attributes of the svg separatedly that we can later manipulate in our components. We are also filtering the SVG using the well known SVGO utility svgo-loader, we can extend or add more filtering options to optimize our SVGs thanks to it.

We are also restricting this loader to the icons folder, just in case we are handling the other SVGs in our app differently, but of course, you can use it for all SVGs removing the include key.

React

Make it work in React is very straight forward. We need to add the loader to our Webpack config, then add an icons folder and the Icon component.

import React from 'react';
import PropTypes from 'prop-types';

const defaultSize = '100%';

const Icon = ({ name, size, color }) => (
  <svg
    xmlns={name.attributes.xmlns}
    viewBox={name.attributes.viewBox}
    style={{ height: size, width: 'auto', fill: color }}
    dangerouslySetInnerHTML={{ __html: name.content }}
  />
);

Icon.propTypes = {
  size: PropTypes.string,
  color: PropTypes.string,
};

Icon.defaultProps = {
  size: defaultSize,
  color: null,
};

export default Icon;

That’s it. Our React component takes as props: the name of the imported module of the SVG, the size, and the color. If not given, the SVG will inherit the fill color set in the parent element (or itself). Also, if not specified, the SVG will scale to the parent container height.

Take a look into the JSX of the example

<div style={{ height: '100px' }}>
    <Icon name={Add} />
</div>
<Icon name={Add} size="45px" />
<Icon name={Add} size="45px" color="red" />
<Icon name={Plone} size="60px" color="#1782BE" />
<Icon name={Guillotina} size="60px" color="#EC5528" />

Angular

For the angular icon component we needed the same recipe for the Webpack config and this icon component.

import {
    Component,
    Input,
    HostBinding,
    ViewEncapsulation,
    ChangeDetectionStrategy } from '@angular/core';

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { OnInit } from '@angular/core';

const defaultSize = '100%';

@Component({
  // tslint:disable-next-line:component-selector
  selector: '[icon], icon',
  template: `
  <svg
    [attr.viewBox]="name.attributes.viewBox"
    [attr.xmlns]="name.attributes.xmlns"
    [innerHTML]="svgContent"
    [style.height]="height"
    [style.width]="'auto'"
    [style.fill]="color"
    >
  </svg>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class IconComponent implements OnInit {

  constructor(private sanitizer: DomSanitizer) {}

  svgContent: SafeHtml;
  defaultSize = defaultSize;
  height: string;

  @Input() color: string;
  @Input() size: string;
  @Input() name;

  ngOnInit() {
    this.svgContent = this.sanitizer.bypassSecurityTrustHtml(this.name.content);
    this.height = this.size ? this.size : defaultSize;
  }

}

We also use the same approach using the component, the Angular template way:

<div icon [name]="Add"></div>
<div icon [name]="Add" color="green"></div>
<icon [name]="Add" color="red" size="45px"></icon>
<icon [name]="Plone" color="#1782BE" size="60px"></icon>
<icon [name]="Guillotina" color="#EC5528" size="60px"></icon>

Our Angular component takes the same three properties as the React one.

In addition, Typescript forces us to overcome some tiny things.

Typings

In order to be able to import the SVG as a module, we need to add this typing to our app:

declare module "*.svg" {
  const content: any;
  export default content;
}

Add the imported SVG object as a Class member

The Angular template won’t be able to use it if the imported SVG object is not a Class member, like:

import { Component } from '@angular/core';
import Add from '../icons/add.svg';
import Plone from '../icons/plone.svg';
import Guillotina from '../icons/guillotina.svg';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  Add = Add;
  Plone = Plone;
  Guillotina = Guillotina;
}

Conclusion

While there are other approaches out there like the Icon component that @angular/material has, they all feel to me like too much and all of them are bloated with lots of options that we don’t really need. I’d like to use a more lightweight and approachable solution like the exposed here that only does what we really need. At the end, it’s not rocket science.

If you have any suggestion, please contact me or open an issue on Github. PRs are welcome!


Timo Stollenwerk

Víctor Fernández de Alba works at kitconcept. He has been a Plone developer for ten years and a Plone core developer for six. Currently serving on the Plone Board of Directors, he is also member of the plone.org and frontend teams. He also was the co-organizer of the Barcelona Plone Conference 2017.