A journey to writing vanilla web components

Part 1: Using Attributes

First things first, MDN is our friend.

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

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

// custom-chip.jsexport CUSTOM_CHIP_TAGNAME = 'custom-chip';const template = document.createElement('template')
template.innerHTML = `
<slot>Default Chip Text.</slot>
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;
? 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 () {
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

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);
.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

/* 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 = `
: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

  • 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

Front-end Developer; I‘m not sure who I am.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store