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!
It was clear to me that font-based icon systems are a no longer an option today. For several reasons:
only to name a few.
The rise of SVG in modern web developing is for a good reason:
<img />
tagMy 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.
Our main goal is to provide inline SVGs in our applications, having in mind:
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>
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.
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.
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 = {
name: PropTypes.shape({
xmlns: PropTypes.string,
viewBox: PropTypes.string,
content: PropTypes.string,
}).isRequired,
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" />
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.
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;
}
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;
}
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!
Do you want to see Plone 6 (Volto) in action? No matter if you are a developer interested in Volto or a company thinking about using Volto in an upcoming project. We are happy to give you a tour. Just drop us a note.
Víctor Fernández de Alba is the CTO of 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.