logo
post image

How to Create a HTML Custom Element

What is a Custom Element ?

A custom element is a new HTML element that we can create ourselves. Unlike browser built-in elements like <p>, <button> etc, custom elements are created through custom HTML, Javascript & CSS.

Once a custom element is created, it can be used like a normal HTML element in the page. Multiple instances can be declared in the page, Javascript DOM methods can be used (e.g. document.querySelector), event listeners can be attached & so on.

Example Custom Element For This Tutorial

We will create a simple custom element having the tag <input-plus-minus>.

<input-plus-minus></input-plus-minus>

This element behaves like a "quantity button" that allows to increase or decrease a given quantity by 1. This functionality is commonly found in shopping carts.

Here is a working demo of this custom element :

Custom Element Has Its Own HTML, CSS & Javascript

A custom element has its own HTML, CSS & Javascript.

Taking the example of <input-plus-minus> :

  • Its HTML contains a text input type & 2 buttons.
  • It has CSS rules for styling text input & buttons.
  • Its Javascript contains click event handlers on each button that changes the value in the textbox.

Shadow DOM

The custom element's HTML, CSS & Javascript may clash with the code present in other sections of the page. To prevent this, we need the element's code kept isolated to itself.

The browser does exactly this — it isolates the HTML, CSS & Javascript of the custom element from the rest of the page. This isolation is implemented in the form of Shadow DOM. Consider the Shadow DOM to be a "mini-DOM" holding just the DOM inside the custom element.

Defining Custom Element's HTML & CSS

We can define the element's HTML & CSS in a <template> tag. This tag can hold custom HTML content, which is kept hidden by default. The content can be later made visible using Javascript.

Using a <template>, we define HTML & CSS for <input-plus-minus>.

<template id="input-plus-minus-template">
	<style>
	button {
		width: 25%;
		/* other CSS */
	}

	input {
		width: 50%;
		/* other CSS */
	}
	</style>
	<div id="container">
		<button id="subtract">-</button>
 	 	<input type="text" id="count" value="1" readonly />
 	 	<button id="add">+</button>
 	</div>
</template>

Due to the isolation provided by the Shadow DOM, we can be sure that the classes, IDs, etc defined inside the custom element won't clash with the rest of the page.

We have handled the custom element's HTML & CSS. Now we will look at the Javascript involved.

Registering the Custom Element With Javascript

Before the custom element can be used, it needs to be registered using Custom Elements API. In browser, this API is implemented by the global property customElements.

The customElements.define() method registers a new custom element. Parameters required are the element's tag name & a Javascript class that extends the base HTMLElement.

The code below is the bare minimum Javascript required to create a custom element.

// custom element's class
class InputPlusMinus extends HTMLElement {
	constructor() {
		super();

		// rest of the Javascript functionality
	}
}

// register the custom element <input-plus-minus>
customElements.define('input-plus-minus', InputPlusMinus);

In the next step we will see how to include element's specific Javascript (event handlers etc).

Defining Custom Element's Specific Javascript

The custom element's specific Javascript can be included in the same class that was used to create it.

Due to the isolation provided by the Shadow DOM, the element's Javascript cannot clash with other Javascript in the page.

  • First we need to render the custom element's HTML & CSS from the <template> tag.

    /* this code is present inside the defined class */
    
    let template = document.querySelector('#input-plus-minus-template').content;
    this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true));
    

    this inside the class refers to the current element.
    attachShadow() method attaches a Shadow DOM to the element and returns the root of the created Shadow DOM. To that we append contents of the <template> tag.
    The mode open means that the element's Shadow DOM can be accessed outside the element (using Javascript). Optionally it is also possible to lock the element's Shadow DOM from being accessed.

  • We now need to implement event handlers for the 2 buttons inside the element that will increase / decrease current value in the textbox.

    // access #add within the custom element's DOM
    let add_button = this.shadowRoot.querySelector("#add");
    
    let subtract_button = this.shadowRoot.querySelector("#subtract");
    let count_textbox = this.shadowRoot.querySelector("#count");
    
    // set initial "value" attribute
    this.setAttribute('value', '1');
    
    // add 1 quantity
    add_button.addEventListener('click', () => {
    	let current = parseInt(count_textbox.value, 10);
    	count_textbox.value = current + 1;
    	
    	// set "value" attribute of the custom element
    	this.setAttribute('value', count_textbox.value);
    });
    
    // subtract 1 quantity
    subtract_button.addEventListener('click', () => {
    	let current = parseInt(count_textbox.value, 10);
    	if(current > 1) {
    		count_textbox.value = current - 1;
    		this.setAttribute('value', count_textbox.value);
    	}
    });
    

    shadowRoot property refers to root of the element's Shadow DOM.
    Just like document.querySelector() can be used to access elements inside the page, this.shadowRoot.querySelector() can be used to access elements inside the custom element.
    For this example element, we choose the "value" attribute to hold its current value. This attribute can be used by outside code to get the current value of the element (using getAttribute).
    Arrow functions are used for click event callbacks. This ensures that this inside the callback refers to the element itself.

With this, the Javascript for our custom element is complete. We can now start using the custom element.

<input-plus-minus id="sample"></input-plus-minus>

Complete Code

<template id="input-plus-minus-template">
	<style>
	button {
		width: 25%;
		/* other CSS */
	}

	input {
		width: 50%;
		/* other CSS */
	}
	</style>
	<div id="container">
		<button id="subtract">-</button>
 	 	<input type="text" id="count" value="1" readonly />
 	 	<button id="add">+</button>
 	</div>
</template>

<script>
class InputPlusMinus extends HTMLElement {
	constructor() {
		super();

		let template = document.querySelector('#input-plus-minus-template').content;
      	this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true));

      	let add_button = this.shadowRoot.querySelector("#add");
      	let subtract_button = this.shadowRoot.querySelector("#subtract");
      	let count_textbox = this.shadowRoot.querySelector("#count");

      	this.setAttribute('value', '1');

      	add_button.addEventListener('click', () => {
			let current = parseInt(count_textbox.value, 10);
			count_textbox.value = current + 1;
			this.setAttribute('value', count_textbox.value);
      	});

      	subtract_button.addEventListener('click', () => {
      		let current = parseInt(count_textbox.value, 10);
      		if(current > 1) {
      			count_textbox.value = current - 1;
      			this.setAttribute('value', count_textbox.value);
      		}
      	});
	}
}

customElements.define('input-plus-minus', InputPlusMinus);
</script>

<!-- custom element being used -->
<input-plus-minus id="sample"></input-plus-minus>

We have set the "value" attribute to hold the current value. This can be used to get the current value of the element.

document.querySelector("#sample").getAttribute('value');