How to change SVG color on interaction with LitElement

Luis Aviles

Web Components have been around for a couple of years now. According to the most recent MDN documentation:

Web Components is a suite of technologies that allow you to create custom elements, whose functionality is encapsulated and separated from the rest of the source code, for use in web applications.

On other hand, SVG content is the preferred way to add logos, icons, and other graphic resources on the web. Mainly because of their speed, accessibility, and resolution among other benefits.

In this Media Jam, we'l use LitElement features and TypeScript to process interactions that allow changing the SVG colors in a practical way.

The Problem

Let's suppose you're working in a web application that needs to render some web components that are built using SVG content. However, it's needed to capture the user interaction to provide a change of the state of your widget using a different color. See the next image for a better understanding.

As you may think, we can implement these widgets using the Web Components approach and provide an external component to trigger the state change. Let's get started.

The Solution

The SVG Icons

Let's get started using a couple of SVG icons. Instead of creating .svg files, we can define this content as lit-html templates.

lit-html is an efficient, expressive, extensible HTML templating library for JavaScript.

This is the template library to be used to render the custom components later.

  • Start creating the svg/svg-heart.ts file
1// svg-heart.ts
2
3import { html } from 'lit-html';
4
5export const corpIconHeart = html`
6 <svg
7 width="24"
8 height="24"
9 viewBox="0 0 24 24"
10 fill="none"
11 xmlns="http://www.w3.org/2000/svg"
12 >
13 <path
14 d="M12 21.35L10.55 20.03C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3C9.24 3 10.91 3.81 12 5.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5C22 12.27 18.6 15.36 13.45 20.03L12 21.35Z"
15 />
16 </svg>
17`;
  • Now create an alternative icon in the svg/svg-user.ts file.
1// svg-user.ts
2
3import { html } from 'lit-html';
4
5export const corpIconUser = html`
6 <svg
7 width="24"
8 height="24"
9 viewBox="0 0 24 24"
10 fill="none"
11 xmlns="http://www.w3.org/2000/svg"
12 >
13 <path
14 d="M7.5 6.5C7.5 8.981 9.519 11 12 11C14.481 11 16.5 8.981 16.5 6.5C16.5 4.019 14.481 2 12 2C9.519 2 7.5 4.019 7.5 6.5ZM20 21H21V20C21 16.141 17.859 13 14 13H10C6.14 13 3 16.141 3 20V21H20Z"
15 />
16 </svg>
17`;

Creating the Icon Component

We can create a small component as an abstraction to load any icon defined in the application. Let's create it under the corp-icon.ts file with the following content.

1import {
2 LitElement,
3 html,
4 property,
5 customElement,
6 css,
7 TemplateResult,
8} from 'lit-element';
9
10@customElement('corp-icon')
11export class CorpIcon extends LitElement {
12 static styles = css`
13 :host {
14 display: inline-block;
15 }
16 .corp-button {
17 padding: 10px;
18 border-radius: 5px;
19 cursor: pointer;
20 }
21 `;
22
23 @property({ type: String }) icon?: TemplateResult;
24 @property({ type: String }) color?: string;
25
26 constructor() {
27 super();
28 }
29
30 render() {
31 return html`
32 <div class="corp-button">
33 ${this.icon}
34 </div>
35 `;
36 }
37}

Using the @property declaration from LitElement, we'll be rendering the template every time the given property changes.

To have more control over the possible changes over any of these attributes, we can override the updated function as follows.

1// corp-icon.ts
2 updated(changedProperties: Map<string, unknown>) {
3 if (changedProperties.has('color')) {
4 const svg = this.shadowRoot.querySelector('svg');
5 let color: string;
6 switch (this.color) {
7 case 'primary':
8 color = '#0066FF';
9 break;
10 default:
11 color = '#555555';
12 break;
13 }
14 svg.style.fill = color;
15 }
16 }

You can use the updated function(which is part of the component's lifecycle) to:

  • Identify a property change
  • Perform any post-updating task

Also, let's understand what's happening in the above code snippet:

  • changedProperties.has('color') verifies if the property color has been changed.
  • Once the above property changes, we'll get access to the <svg> element in the DOM through the this.shadowRoot.querySelector('svg') call.
  • According to the new state of the color attribute(this.color) we'll perform an update assigning the primary or a default value.
  • Finally, the svg.style.fill = color; line will override any color value with the new one. It should be equivalent to have something like this in your CSS:
1svg {
2 fill: color
3}

Creating the Container Component

The container component, again, will be an abstraction to wrap all the UI elements in the application. Let's create the corp-container.ts file.

1// corp-container.ts
2import {
3 LitElement,
4 html,
5 customElement,
6 css,
7} from 'lit-element';
8import { corpIconUser } from './svg/svg-user';
9import { corpIconHeart } from './svg/svg-heart';
10import './corp-icon';
11
12@customElement('corp-container')
13export class CorpContainer extends LitElement {
14 static styles = css`
15 :host {
16 display: block;
17 }
18 .container {
19 display: flex;
20 flex-direction: column;
21 }
22 .controls {
23 display: flex;
24 flex-direction: row;
25 }
26 `;
27
28 color: string = 'basic';
29
30 constructor() {
31 super();
32 }
33
34 render() {
35 return html`
36 <div class="container">
37 <div>
38 <corp-icon .icon=${corpIconUser} .color=${this.color}> </corp-icon>
39 <corp-icon .icon=${corpIconHeart} .color=${this.color}> </corp-icon>
40 </div>
41 </div>
42 `;
43 }
44}

There are a couple of important notes to explain here:

  • The import './corp-icon'; line will import the definition of the CorpIcon component.
  • The CorpContainer class extends from LitElement, which is the base class provided by the library.
  • The static styles line provides a context to define the styles needed by the local component.
  • The render() function will return a template through lit-html
    • Inside this function, you can define your template using the existing HTML elements and your custom ones.
    • The <corp-icon> element is used here, and also pay attention to the JavaScript expressions to be evaluated for the icon and the color attributes.
    • In LitElement, it's used the dot notation to bind an expression with a component property(ie. .icon=${corpIconUser})

Adding an Interaction

At this point, the app is able to render a couple of icons with a predefined style. However, it would be good to add a couple of controls to change their state.

Let's update the render function in the corp-container.ts file to add two buttons:

1render() {
2 return html`
3 <div class="container">
4 <div class="controls">
5 <button @click=${() => this.changeColor('primary')}>Primary</button>
6 <button @click=${() => this.changeColor('Basic')}>Basic</button>
7 </div>
8 <div>
9 <corp-icon .icon=${corpIconUser} .color=${this.color}> </corp-icon>
10 <corp-icon .icon=${corpIconHeart} .color=${this.color}> </corp-icon>
11 </div>
12 </div>
13 `;
14 }
15
16 private changeColor(color: string) {
17 this.color = color;
18 }

These new buttons are meant to provide the user a way to interact with the icons and change the style from Primary to Basic and vice versa.

The LitElement-way to provide this behavior is with a custom function and the event handler binding using the @ notation: @click=${function()}. This means the function will be called every time the user interacts with a click over both buttons.

This code may not work at this time if you are interacting with the buttons already, since we have to find a way to say: "We changed a property's value. It's needed to perform and update".

This can be done automatically with LitElement using the @property and @internalProperty decorators. And since this container doesn't need to define public properties, we can use the second one, and apply it to the color attribute as follows:

1// corp-container.ts
2
3@customElement('corp-container')
4export class CorpContainer extends LitElement {
5 // styles
6
7 @internalProperty() color: string = 'basic';
8
9 render() {
10 // returns the template
11 }
12}

The rendering issue will be fixed now since LitElement will make sure to trigger an "update" for the CorpIcon component.

If you feel curious about the differences between @property and @internalProperty decorators, take a look into the LitElement properties: @proeprty vs @internalProperty article.

Live Demo

This project is available on CodeSandbox.

If you prefer, you can play around with the project here too:

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

Luis Aviles

Senior Software Engineer

Luis is a Senior Software Engineer and Google Developer Expert in Web Technologies and Angular. He is an author of online courses, technical articles, and a public speaker. He has participated in different international technology conferences, giving technical talks, workshops, and training sessions. He’s passionate about the developer community and he loves to help junior developers and professionals to improve their skills.
When he’s not coding, Luis is doing photography or Astrophotography.