Home Articles

The right way to componentize SVGs for your Angular app

Nov 15, 2021 ∘ Yoko Ishioka
SVG icons

Last year, I wrote an article about how to create an SVG library in Angular by making components that link to SVG files instead of HTML templates. Shortly afterwards, my boss tasked me with finding a solution to the excessive DOM size problem, which we discovered was being caused by all of the elements that make up our SVGs.

How to create references instead of actual clones to your SVGs

After doing extensive research and trying different methods, I came across the 'symbol' and 'use' HTML elements. This article gives great in-depth explanations into why this is the correct approach.

If you have ever worked in Flash...I mean Animate, you will remember how awesome symbols were. They enabled you to create one symbol and then reference that symbol so any changes you did to the original were cascaded to all of its references. By using 'symbol' and 'use', you are essentially doing the same thing.

To make this solution even more enticing, the 'use' element acts as a container and stores its children in the shadow DOM. In other words, your SVG elements no longer count towards your DOM size.

The drawbacks of the shadow DOM and how to overcome them

There are some drawbacks though with using a closed shadow DOM. For example, you can't access or manipulate the SVG's viewBox through JavaScript. Therefore, you won't be able to do things like pull out the SVG's exact dimensions without inspecting the element through a browser or updating the paths of the SVG without an image editor.

This can be problematic because if you don't set the SVG's width and height, it will automatically have a width of 300 pixels and a height of 200 pixels. If your SVG is considerably smaller, you will see a lot of blank space around the SVG. Because we're going to create one component for all of your SVGs, you can't set one universal viewBox to handle this problem.

The viewBox isn't the only thing that's inaccessible. All of the SVG's styles are as well. This is actually a pretty good benefit because a change to the original would be applied to all of its references.

Don't fret; we can easily overcome these hurdles by making our SVG generic and adaptable. We can use properties like 'inherit' on the SVG's width and height and 'currentColor' for its fill and outline so they can be set through a parent or ancestor.

How to create the SVG component

Step 1: Create a graphic and export it as an SVG

Note: In order to individually style your SVG and have it scale gracefully to any size, you must use actual paths instead of embedded images for your SVGs.

For example, I made my logo with vector art using Illustrator. This is what it looks like when I export just the cloud as an SVG:

<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229 149.04">
  <path d="M189.37,142a12.35,12.35,0,0,1-3.2-3c-1.26-1.65-2.31-3.5-3.71-5a12.12,12.12,0,0,0,2.48,4.77A12.55,12.55,0,0,0,189.37,142Z" transform="translate(0 0)" style="fill:#ccc"/>
  <path d="M192.89,142.94a12.66,12.66,0,0,1-3.52-1A6.19,6.19,0,0,0,192.89,142.94Z" transform="translate(0 0)" style="fill:#ccc"/>
  <path d="M182.46,133.91a11.83,11.83,0,0,1-.4-3.65,5,5,0,0,0-1.2,2.21,1.51,1.51,0,0,1,.26.18A10.64,10.64,0,0,1,182.46,133.91Z" transform="translate(0 0)" style="fill:#ccc"/>
  <path d="M229,76.35a28.33,28.33,0,0,0-19-26.61,14.29,14.29,0,0,0,1.32-6,14.8,14.8,0,0,0-14.93-14.67h-.09a15.07,15.07,0,0,0-10.47,4.2,27.92,27.92,0,0,0,.45-5C186.28,12.67,173.36,0,157.43,0a28.94,28.94,0,0,0-25.81,15.64,24.52,24.52,0,0,0-2.7-.2A29.22,29.22,0,0,0,113.33,11C97.4,11,84.48,23.62,84.48,39.25a28.41,28.41,0,0,0,.15,2.92c-2.79-.57-5.76-1.72-8.51-2.21-4.42-.54-8.85,2.71-12.19,5.47a36.36,36.36,0,0,0-3.47,3.66A31.66,31.66,0,0,0,52.9,67.61c-.56,13,10,21.14,18.8,29.27,1.1,1.63,0,4.88,1.1,5.42,3.32,3.26,6.64-2.71,6.08-6-1.1-4.88-6.08-6-8.84-9.22-3-3.25-2.91-7.58-1-11.31a14.81,14.81,0,0,1,2.49-3.44c3.45-4.22,9-3.68,13.41-2.59,7.19,2.17,14.93,6,16.6,13.59-.06,1.62-.15,3.29-.28,5-1.06,13.49-4.32,26.82-15.67,35.58-8.37,6.46-19.16,9.14-29.75,10.3s-21.32.93-31.87,2.4c14.49-4.51,29.91-5.73,44.28-10.58,7.48-2.53,14.82-6.18,19.89-12.12a35.54,35.54,0,0,0,7.68-17.8c1.05-6.79.27-14.5-4.81-19.23-3.5-3.26-9.84-4.22-12.87-.53s-1.07,9,1.5,12.82A19.63,19.63,0,0,1,78,83.06a6.67,6.67,0,0,1,2.37-5.66c2.14-1.58,5.3-1.22,7.48.29a13.06,13.06,0,0,1,4.46,6.37,28.19,28.19,0,0,1-3,25.38C82.12,120,68.58,124.6,55.72,126.23,46.5,127.4,37.17,127.4,28,128.57S-12.06,144.48,5.1,148c16,3.23,88.05-1.41,106.45-9.34s34.35-20.07,54.15-24.38c5-1.09,9.4,0,13.82,2.71,7.19,4.34,6.08,11.92,6.08,18.43,0,2.17,2.21,4.88,4.43,4.88,7.74,0,13.82-7.59,14.37-14.64-.56,7.19-5.48,20.7-15.52,16.76-2.32-.91-4.12-2.73-5.85-4.5a6.9,6.9,0,0,0,2.39,3.76,13.57,13.57,0,0,1-2.71-2.53,9,9,0,0,1-2-4.36,6,6,0,0,1,.14-2.27,1.53,1.53,0,0,0-.51-.21,1.2,1.2,0,0,0-1.17.87c-1.87,5.72,7.7,10.77,12.16,11.31,9.91,1.22,17.12-7.21,19.47-15.79a43.86,43.86,0,0,0,1.13-5.79,23.09,23.09,0,0,0,0-7.42c-.18-1.22-.43-2.44-.72-3.66a21.87,21.87,0,0,0-1.13-4.29c-.48-1.38-1-2.76-1.39-4.12A28.37,28.37,0,0,0,229,76.35Zm-39.19,67.34a21.89,21.89,0,0,0,5.3.41,13.85,13.85,0,0,1-5.32-.41Z" transform="translate(0 0)" style="fill:#ccc"/>
 </svg>

Step 2: Prepare your SVG

The next step is to add 'symbol' tags to the elements inside of your SVG and to move the viewBox to the newly created 'symbol' element. You must also add an id attribute to the symbol so that you can call it externally.

If you want to change the colors differently for each reference, you have to set the 'fill' and 'outline' attributes to 'currentColor' or 'inherit'. Use 'currentColor' to set the color through the 'color' property. Use 'inherit' to set the color through the 'path' property.

I also learned a trick from using Font Awesome icons. You can automatically make the SVG be two-colored by setting the opacity of the secondary color.

Now, your SVG will look something like this:

<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="ces-logo-cloud"  viewBox="0 0 228.98 148.98">
    <path opacity="0.4" d="M189.37,142a12.22,12.22,0,0,1-3.2-3c-1.26-1.65-2.31-3.5-3.71-5a12.12,12.12,0,0,0,2.48,4.77A12.6,12.6,0,0,0,189.37,142Z"/><path opacity="0.4" d="M192.89,142.94a12.74,12.74,0,0,1-3.52-1A6.2,6.2,0,0,0,192.89,142.94Z"/><path opacity="0.4" d="M182.46,133.91a11.83,11.83,0,0,1-.4-3.65,5,5,0,0,0-1.2,2.21,2.4,2.4,0,0,1,.26.18A10.68,10.68,0,0,1,182.46,133.91Z"/><path fill="currentColor" d="M229,76.35a28.34,28.34,0,0,0-19-26.61,14.29,14.29,0,0,0,1.32-6A14.8,14.8,0,0,0,196.3,29.07a15.06,15.06,0,0,0-10.47,4.2,28.12,28.12,0,0,0,.45-5C186.28,12.67,173.36,0,157.43,0a28.92,28.92,0,0,0-25.81,15.64,24.52,24.52,0,0,0-2.7-.2A29.18,29.18,0,0,0,113.33,11C97.4,11,84.48,23.62,84.48,39.25a28.26,28.26,0,0,0,.15,2.92c-2.79-.57-5.76-1.72-8.51-2.21-4.42-.54-8.85,2.71-12.19,5.47a37.46,37.46,0,0,0-3.47,3.66A31.68,31.68,0,0,0,52.9,67.61c-.56,13,10,21.14,18.8,29.27,1.1,1.63,0,4.88,1.1,5.42,3.32,3.26,6.64-2.71,6.08-6-1.1-4.88-6.08-6-8.84-9.22-3-3.25-2.91-7.58-1-11.31a14.86,14.86,0,0,1,2.49-3.44c3.45-4.22,9-3.68,13.41-2.59,7.19,2.17,14.93,6,16.6,13.59-.06,1.62-.15,3.29-.28,5-1.06,13.49-4.32,26.82-15.67,35.58-8.37,6.46-19.16,9.14-29.75,10.3s-21.32.93-31.87,2.4c14.49-4.51,29.91-5.73,44.28-10.58,7.48-2.53,14.82-6.18,19.89-12.12a35.52,35.52,0,0,0,7.68-17.8c1.05-6.79.27-14.5-4.81-19.23-3.5-3.26-9.84-4.22-12.87-.53s-1.07,9,1.5,12.82A19.62,19.62,0,0,1,78,83.06a6.66,6.66,0,0,1,2.37-5.66c2.14-1.58,5.3-1.22,7.48.29a13.1,13.1,0,0,1,4.46,6.37,28.18,28.18,0,0,1-3,25.38C82.12,120,68.58,124.6,55.72,126.23,46.5,127.4,37.17,127.4,28,128.57S-12.06,144.48,5.1,148c16,3.23,88.05-1.41,106.45-9.34s34.35-20.07,54.15-24.38c5-1.09,9.4,0,13.82,2.71,7.19,4.34,6.08,11.92,6.08,18.43,0,2.17,2.21,4.88,4.43,4.88,7.74,0,13.82-7.59,14.37-14.64-.56,7.19-5.48,20.7-15.52,16.76-2.32-.91-4.12-2.73-5.85-4.5a6.88,6.88,0,0,0,2.39,3.76,13.55,13.55,0,0,1-2.71-2.53,8.92,8.92,0,0,1-2-4.36,6,6,0,0,1,.14-2.27,1.42,1.42,0,0,0-.51-.21,1.19,1.19,0,0,0-1.17.87c-1.87,5.72,7.7,10.77,12.16,11.31,9.91,1.22,17.12-7.21,19.47-15.79a44.22,44.22,0,0,0,1.13-5.79,23.09,23.09,0,0,0,0-7.42c-.18-1.22-.43-2.44-.72-3.66a21.43,21.43,0,0,0-1.13-4.29c-.48-1.38-1-2.76-1.39-4.12A28.37,28.37,0,0,0,229,76.35Zm-39.19,67.34a21.82,21.82,0,0,0,5.3.41A13.73,13.73,0,0,1,189.79,143.69Z"/>
  </symbol>
</svg>

Step 3: Create a component that will fetch your SVGs

svg.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { SvgSize } from './svg';

@Component({
  selector: 'ces-svg',
  templateUrl: './svg.component.html',
  styleUrls: ['./svg.component.scss']
})
export class SvgComponent implements OnInit {

link: string = '';

@Input() size: SvgSize | undefined;
@Input() type: string = '';
@Input() dropShadow: boolean = true;

constructor() {
}

ngOnInit(): void {
  this.link = `/assets/images/svgs/${this.type}.svg#${this.type}`;
}

It's crucial to remember you can't just link a file to the SVG. You must add a hash tag and the name of your id to the end of the file name after the file extension. You can save on http requests if you house multiple SVGs in one file, aka sprites. But you must use unique ids for each SVG in the file.

Also, if you want to add classes to apply styles that fit exactly to the shape of the SVG like drop shadows or borders, you have to add them directly to the SVG element or else they will be applied to the component host. However, you can define those classes in the component or as a global class.

svg.component.html

<svg [ngClass]="{ 'drop-shadow' : dropShadow }" [class]="size ? size : ''" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
  <use [attr.xlink:href]="link" [attr.href]="link" />
</svg>

I'm specifying the two href attributes because 'xlink:href' is being deprecated and is just there for backwards compatibility.

svg.component.scss

@import 'variables';

svg {
  width: inherit;
  height: inherit;
}

.xs {
  width: $icon-xs-sm;
  height: $icon-xs-sm;
}

.sm {
  width: $icon-sm-sm;
  height: $icon-sm-sm;
}

.md {
  width: $icon-md-sm;
  height: $icon-md-sm;
}

.lg {
  width: $icon-lg-sm;
  height: $icon-lg-sm;
}

@media (min-width: $breakpoint-md) {
  .xs {
    width: $icon-xs-md;
    height: $icon-xs-md;
  } 

  .sm {
    width: $icon-sm-md;
    height: $icon-sm-md;
  }

  .md {
    width: $icon-md-md;
    height: $icon-md-md;
  }

  .lg {
    width: $icon-lg-md;
    height: $icon-lg-md;
  }
}

Don't forget to set the width and height to 'inherit' using the 'SVG' selector or else you won't be able to set them outside of the component.

In order to get around the problem of not being able to view or set the viewBox per reference, I use art boards with square dimensions when exporting SVGs. That way, I don't have to worry about getting the exact width/height ratio correct and can easily avoid distorted graphics.

In order to enforce standardized sizes, I also created different classes for each size. I defined them using variables so different apps that use this component can have their own standardized sizes.

Example Usage

Alas, here is an example of how to use the SVG component we just created in a button:

<button class="inherit" [ngClass]="{ 'active' :  active }" (click)="toggleEdit()">
  <ces-svg size="xs" name="pencil"></ces-svg>
</button>

The SVG's styles that we set earlier will automatically inherit those of the button. But they can also be set by adding a class to the SVG component.

Source Files

Here is a link to the source files. Please send me any thoughts or ideas for improvement. Thanks!