A journey to writing vanilla web components

Part 1: Using Attributes

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)

Let’s begin.

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)

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>
export class HTMLCustomChip extends HTMLElement {
static get observedAttributes () { … }
// 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 });
// 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');
// 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
// 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`))
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;
// 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() */
Yey, we did it ! End of part 2 !
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 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.

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