A journey to writing vanilla web components

Part 1: Using Attributes

Thomas Juster
4 min readJul 8, 2020

First things first, MDN is our friend.

According to MDN, Web Components aims at achieving code re-usability. It consists of three main technologies − I’m completely paraphrasing here −:
- Custom Elements (see MDN tutorial)
- Shadow DOM (see MDN tutorial)
- HTML Template and Slot (see MDN tutorial)

MDN also has a repository containing a lot of examples.

Let’s begin.

Today, we will focus on using attributes. If you come from the React world, attributes can be used to pass on primitive type variables (strings, booleans, numbers, …)

Building a badge/chip component

Let’s start small and easy by building a chip component:

A regular chip (left) and a rounded chip (right)

So. Our chip will meet the following requirements:
- It can be rounded or normal
- It should be stylable from the main CSS − because CSS in the shadow DOM is independent

Step 1/2 − Implementing the ‘rounded’ attribute

Let’s start by creating the component, here’s the skeleton:

// custom-chip.jsexport CUSTOM_CHIP_TAGNAME = 'custom-chip';const template = document.createElement('template')
template.innerHTML = `
<slot>Default Chip Text.</slot>
<style>…</style>
`
export class HTMLCustomChip extends HTMLElement {
static get observedAttributes () { … }
}

Now we tell the browser that we will watch the rounded attribute:

// custom-chip.js
export class HTMLCustomChip extends HTMLElement {
static get observedAttributes () {
// NOTE: Here, you can any attribute
// they can be yours or others like 'style' or 'class'
return ['rounded'];
}
attributeChangedCallback (attribute, previousValue, nextValue) {
console.info({ attribute, previousValue, nextValue });
}
}

For convenience, we can also define a getter and a setter using the regular HTMLElement API:

// custom-chip.js
export class HTMLCustomChip extends HTMLElement {
static get observedAttributes () {…}
get rounded () {
return this.hasAttribute('rounded');
}
set rounded (value) {
if (this.rounded === value) return;
value
? this.setAttribute('rounded', '')
: this.removeAttribute('rounded');
}
}

Let’s describe what the component should do when it is inserted into the document:

// custom-chip.js
export class HTMLCustomChip extends HTMLElement {

constructor () {
super();
this.attachShadow({ mode: 'open' });
// mode: 'open' allows global javascript to access
// the inside of our shadow DOM.
// Attaching a shadow populated the member 'shadowRoot'
// of an HTMLElement
const shouldCloneDeeply = true
this.shadowRoot.appendChild(
template.content.cloneNode(shouldCloneDeeply)
);
}
}

Now let’s define our custom-chip so that the browser knows how to interpret a html tag <custom-chip />:

// main.js
import { CUSTOM_CHIP_TAGNAME, HTMLCustomChip } from "./custom-chip.js";
if (!window.customElements.get(CUSTOM_CHIP_TAGNAME)) {
window.customElements.define(CUSTOM_CHIP_TAGNAME, HTMLCustomChip);
}
window.customElements.whenDefined(CUSTOM_CHIP_TAGNAME)
.then(() => console.info(`${CUSTOM_CHIP_TAGNAME} is now defined`))

Aaand … step 1 is done, congrats! Now you can use the HTML tag <custom-chip rounded />

You, finishing part 1.

Step 2/2 − Adding some styles

First, let’s define CSS variables we will use:

/* style.css − main stylesheet */
:root {
--chip-color: white;
--chip-background: steelblue;
--chip-radius: 3em;
}

Then, let’s use those variables in the component Shadow DOM:

// custom-chip.js

const template = `
<slot></slot>
<style>
:host {
border-radius: 0;
color: var(--chip-color);
background: var(--chip-background);
}
:host([rounded]) {
border-radius: var(--chip-radius);
}
/* with the ':host()' syntax, you can use any CSS selector like a class selector − :host(.my-class) − or any other, not only an attribute selector. See https://developer.mozilla.org/en-US/docs/Web/CSS/:host() */
`

There are 2 important CSS properties to notice here:
- We can style the ‘hosting’ node, here ‘custom-chip’ from the Shadow DOM
- We can use global CSS variables inside the Shadow DOM

NB 1: here we could have styled the custom-chip component using global CSS and by targeting custom-chip {} and custom-chip[rounded] {} instead of :host {} and :host[rounded] {}.

NB2: The chip component could be declared using CSS only, but hey, that’s just a component example.

Well done! Now you know the basics of creating web components.

Yey, we did it ! End of part 2 !

You can find the full demo here.

Beautifully hand-written code

To go further

Keep in mind that the most expensive JavaScript is the one interacting with the DOM. Therefore, be extra-careful when executing DOM operations inside life cycles callbacks.

  • Play with the life cycles callbacks. For instance, in the demo custom-chip.js, remove the if-statement-line of the setter, set multiple time the rounded attribute (by clicking the button in the demo result), and see how the attributeChangedCallback() is triggered every time, even though the previous and next values are the same !
  • Play with slots and the slot name attribute.
  • To improve this component, you could add a variant="primary|danger|warning" attribute and use other global CSS variables.
  • Add observed attributes like class or style and try editing them in the console. See when the attributeChangedCallback() is triggered.
  • Continue this series, I’ll cover layout components and derived components (like custom input implement HTMLInputElement instead of HTMLElement)
You, going way too far.

Next journey: Using slots & templates

--

--