Catalin Ciubotaru avatar

One way of building an SVG icon library for your project

Just one way. Not the only way.

The Problem

Sometimes, especially for a project within a company, you’ll need an icon library, and you can’t use something like font-awsome or feather-icons. This icon-library needs to be easy to extend, easy to use and easy to maintain.

Good to know before jumping in

This article uses Angular for this example, but the same approach can be used for any front end framework. Also for vanilla JavaScript.

The Solution

Our solution might end up looking something like this

<sc-icon name="lightning" size="32px"></sc-icon>

This is pretty straightforward. Again, it’s only a suggestion. There’s no right or wrong approach here.

Part 1 - How do we keep store our SVGs?

An icon is just an SVG, in our case at least. We need a way to keep all our SVGs in one place. Also, this place should be easy to access/change/augment. We can do things with a simple object. Something like this:

export default {
house: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">...</svg>`,
app: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">...</svg>`,
shield: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">...</svg>`,
play: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">...</svg>`
}

For brevity, I didn’t include the actual SVG code, since that would just make the example more difficult to follow.

Ok, so we have an export here that has an object with a bunch of properties, one for each icon. If we want to access the SVG for an icon, we can do something like this: const playSvg = icons[‘play’]; (where we import the file like this import icons from ./icons-svgs.ts

This has a few problems though. One of them is not being type safe 🙅‍♂️. Meaning, you don’t know if there’s a typo in there, you don’t know the name of the icon you want to access, you have no autocomplete. You’ll find all this out at runtime. Not great.

Part 2 - Here comes the Icon Service

To make our lives easier, let’s create a reusable service that will simplify the way we can access our icons. It can look something like this.

import { Injectable } from '@angular/core';

import icons from './icons';

export type IconType = keyof typeof icons;

@Injectable()
export class IconService {
/**
* Return the SVG content for an icon
* @param name Name of the icon to get the SVG for
*/
public getSvgForName(name: IconType): string {
return icons[name];
}
}

Here we are doing 2 things:

  1. We create a type: IconType so we always know which icons are available. This makes the icons typesafe.
  2. We have a method that returns the SVG string to be used in the HTML template.

That’s it. Not a lot of code, but our life is so much better now.

Now, who will use this service? Do we, as consumers of the IconComponent care about it?

Part 3 - Introducing the standalone component

No, we do not.

The actual API of our Icon Library revolves around 1 component. So, the service above is just an implementation detail. Here’s how the consumer of that service might look like:

import { NgClass } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

import { IconService, IconType } from './icon.service';

/**
* The icon component
*/
@Component({
standalone: true,
selector: 'sc-icon',
styleUrls: ['./icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [IconService],
imports: [NgClass],
template: `<span
[innerHTML]="svg"
[style.height]="size"
[style.width]="size">
</span>`
})
export class IconComponent {
/**
* The size of the icon. Accepts valid CSS values
*/
@Input() size = '100%';
/**
* The name of the icon to render
*/
@Input() name: IconType;

protected get svg(): SafeHtml {
const svgContent = this.iconService.getSvgForName(this.name);
return this.sanitizer.bypassSecurityTrustHtml(svgContent);
}

constructor(private sanitizer: DomSanitizer, private iconService: IconService) {}
}

A couple of things are happening here.

First, the component is standalone . That’s the new-ish Angular feature. Very useful in this case, since it allows us to have a VERY atomical component. Very nice!

The ChangeDetection should definitely be OnPush. No reason for any other way.

Then, we’re declaring our inputs. These are a reflection of the desired API we defined in the beginning this post.

  • The name is used to access the correct SVG. Notice how nice is to have this typed.
  • The size is used to allow the consumer to control the space taken by our icon.

After the inputs, we need to use the DomSanitizer to bypass the security while passing along the HTML code for the SVG. This is because otherwise Angular will escape all the "weird" characters to prevent any sort of injection. Security as a priority.

This svg property is being used in the template by biding it to the innerHTML of the span element.

Part 4 - How to extend?

If you want to extend this with a new icon, it couldn’t be simpler. Go the the icons.ts file, add a new property and assign to it the SVG of the new icon. That’s it. From this point on, the new icon is available to you with the name you gave it.

Part 5 - How to replace?

Did you manage to get approval to use feather-icons? Or do you not need custom icons anymore and want to instead use an already available solution? Simple. You have only one entry point here. Your IconComponent can switch implementations, and the consumers need not care about it. Profit!

Part 6 - Congrats!

You made! You read through another post about something mildly interesting. Good job! Hope at least 10% of this was useful to you. 🎉

Want more?

If you want to know more about this, or something doesn't make any sense, please let me know.

Also, if you have questions, you know where to find me… On the internet!

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

One way of building an SVG icon library for your project https://catalincodes.com/posts/one-way-of-building-an-svg-library #Angular

Jan 20, 2023
52 people are talking about this

Wanna read more?

Updates delivered to your inbox!

A periodic update about my life, recent blog posts, how-tos, and discoveries.

No spam - unsubscribe at any time!