· Tomo · Tutorials · 10 min read
How to create custom HTML elements with JavaScript
Let's say you are in a situation where you need to make some web components, but you can't use any of the libraries or frameworks (like React or Vue). You can use custom HTML elements with JavaScript. In this tutorial, we will learn how to create custom HTML elements with JavaScript.
Custom elements are actually HTML elements, but not just any HTML elements. Their functionality is defined by you (the developer) by using JavaScript and the Web Components API. All modern browsers support custom elements, and not much is needed to implement them.
But first, when should you use custom elements? Here are some scenarios where they might be useful:
- Just like in React or Vue components, you want to encapsulate some functionality and reuse it across your project.
- You have a complex JavaScript code, which can be split by responsibility into smaller parts, and then encapsulated into custom elements.
- You want to create a new HTML element that is not available in the standard HTML specification. You can create it just for the sake of describing your HTML structure better.
In this tutorial, we will learn how to create custom HTML elements with JavaScript. We will cover the basics of custom elements, how to create them, and how to use them in your projects. To better understand custom elements, we will attempt to create a simple to-do list application.
The folder structure of our project will look like this:
.
├── index.html
└── js
└── custom-elements.js
You can always split custom-elements.js
into multiple files if your codebase grows. But for the sake of this tutorial, we will keep everything in one file.
Creating a custom element
Let’s start with index.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom HTML elements with JavaScript</title>
</head>
<body>
<h1>Custom HTML elements with JavaScript</h1>
<todo-list> </todo-list>
<script src="js/custom-elements.js"></script>
</body>
</html>
In this file, we have a todo-list
element. I can assure you HTML does not specify this element, so we will create it with JavaScript in our custom-elements.js
file:
class TodoList extends HTMLElement {
constructor() {
super();
console.log('Todo list created!');
}
}
customElements.define('todo-list', TodoList);
In the code above, we created a class TodoList
that extends HTMLElement
, which is the base class for all HTML elements. You can also extend other elements (HTMLButtonElement
, HTMLInputElement
, etc.) if you want to create a custom element that behaves like a specific HTML element. The constructor
method is called when the element is created, and we call super()
to call the constructor of the parent class. Finally, we define our custom element with customElements.define
.
If you preview index.html
in your browser, you should see the message Todo list created!
in the console.
Adding content to the custom element
Each to-do list consists of list items. Each list item should have a checkbox, a label, and a delete button. Since there are multiple responsibilities related to the each list item, we will create a separate custom element for it. Firstable, let’s define the ‘todo-item’ element inside the template
tag in the index.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom HTML elements with JavaScript</title>
</head>
<body>
<h1>Custom HTML elements with JavaScript</h1>
<todo-list>
<!-- START defining the todo-item element template -->
<template>
<todo-item>
<input type="checkbox" />
<label></label>
<button>Delete</button>
</todo-item>
</template>
<!-- END defining the todo-item element template -->
</todo-list>
<script src="js/custom-elements.js"></script>
</body>
</html>
The template
tag is used to define reusable HTML content which is not rendered when the page is loaded. Templates are useful when you want to define a piece of HTML that you want to clone and insert into the DOM multiple times. In our case, we will use the template
tag to define the structure of the todo-item
element.
Now, let’s create the TodoItem
custom element at the end of the custom-elements.js
file:
class TodoItem extends HTMLElement {
constructor() {
super();
console.log('Todo item created!');
}
}
customElements.define('todo-item', TodoItem);
This code looks quite familiar, right? We are creating a new class TodoItem
that extends HTMLElement
, and we define our custom element with customElements.define
.
Now, let’s get one step back and add some IDs to template
tag and its children in the index.html
file:
<template id="ToDoList-Template-ToDoItem">
<todo-item id="ToDoItem-Main">
<input type="checkbox" id="ToDoItem-Checkbox" />
<label id="ToDoItem-Label"></label>
<button id="ToDoItem-DeleteButton">Delete</button>
</todo-item>
</template>
Your IDs can be different, but make sure they are unique (you can also use classes instead, but the implementation will be a bit different). In any case, we will be rewriting these IDs with JavaScript, as we will need them to be unique for each to-do item. Now, let’s make sure our todo-list
knows about the template:
class TodoList extends HTMLElement {
constructor() {
super();
console.log('Todo list created!');
this.elements = {
todoItemTemplate: document.getElementById('[id^="ToDoList-Template-ToDoItem"]'),
};
}
}
customElements.define('todo-list', TodoList);
In the code above, we are storing the todoItemTemplate
element in the elements
object. We are using the ^=
selector to select elements whose id
attribute starts with ToDoList-Template-ToDoItem
. This way, we can select the template element even if the ID is changed (e.g., you can have multiple to-do lists on the same page, each having IDs like ToDoList-Template-ToDoItem-1
, ToDoList-Template-ToDoItem-2
, etc.). Let’s do the same for the child elements of the todo-item
element:
class TodoItem extends HTMLElement {
constructor() {
super();
console.log('Todo item created!');
this.elements = {
checkbox: this.querySelector('[id^="ToDoItem-Checkbox"]'),
label: this.querySelector('[id^="ToDoItem-Label"]'),
deleteButton: this.querySelector('[id^="ToDoItem-DeleteButton"]'),
};
}
}
customElements.define('todo-item', TodoItem);
In the code above, we are storing the checkbox
, label
, and deleteButton
elements in the elements
object. We are again using the ^=
selector to select elements whose id
attribute starts with ToDoItem-Checkbox
, ToDoItem-Label
, and ToDoItem-DeleteButton
.
Let’s add button for adding new to-do items, text input for the to-do item label, and a content wrapper for the to-do items in the index.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom HTML elements with JavaScript</title>
</head>
<body>
<h1>Custom HTML elements with JavaScript</h1>
<todo-list>
<div id="ToDoList-Content"></div>
<input type="text" id="ToDoList-Input" />
<button id="ToDoList-AddButton">Add</button>
<!-- START defining the todo-item element template -->
<template>
<todo-item>
<input type="checkbox" />
<label></label>
<button>Delete</button>
</todo-item>
</template>
<!-- END defining the todo-item element template -->
</todo-list>
<script src="js/custom-elements.js"></script>
</body>
</html>
Now, let’s make sure our todo-list
knows about the button, input and content wrapper elements:
class TodoList extends HTMLElement {
constructor() {
super();
console.log('Todo list created!');
this.elements = {
content: this.querySelector('[id^="ToDoList-Content"]'),
addButton: this.querySelector('[id^="ToDoList-AddButton"]'),
input: this.querySelector('[id^="ToDoList-Input"]'),
todoItemTemplate: document.getElementById('[id^="ToDoList-Template-ToDoItem"]'),
};
}
}
customElements.define('todo-list', TodoList);
Finally, we have everything set to start working on some functionalities for our custom elements.
Adding functionality to the custom elements
Let’s start with our first functionality - adding a new to-do item. We will add an event listener to the addButton
element in the TodoList
class:
class TodoList extends HTMLElement {
constructor() {
super();
console.log('Todo list created!');
this.elements = {
content: this.querySelector('[id^="ToDoList-Content"]'),
addButton: this.querySelector('[id^="ToDoList-AddButton"]'),
input: this.querySelector('[id^="ToDoList-Input"]'),
todoItemTemplate: document.getElementById('[id^="ToDoList-Template-ToDoItem"]'),
};
// Add event listener to the addButton element
this.elements.addButton.addEventListener('click', this.addTodoItem.bind(this));
}
// Add new to-do item
addTodoItem() {
const todoItem = this.elements.todoItemTemplate.content.cloneNode(true);
const label = todoItem.querySelector('[id^="ToDoItem-Label"]');
label.textContent = this.elements.input.value;
this.elements.content.appendChild(todoItem);
this.clearInput();
}
// clear input field
clearInput() {
this.elements.input.value = '';
}
}
customElements.define('todo-list', TodoList);
In the code above, we are adding an event listener to the addButton
element. When the button is clicked, we call the addTodoItem
method. In this method, we clone the content of the todoItemTemplate
element, set the text content of the label element to the value of the input element, append the cloned content to the content
element, and clear the input field by calling the clearInput
method.
When to-do item is added, the TodoItem
constructor is called. Let’s add the delete functionality to the TodoItem
class:
class TodoItem extends HTMLElement {
constructor() {
super();
console.log('Todo item created!');
this.elements = {
checkbox: this.querySelector('[id^="ToDoItem-Checkbox"]'),
label: this.querySelector('[id^="ToDoItem-Label"]'),
deleteButton: this.querySelector('[id^="ToDoItem-DeleteButton"]'),
};
// Add event listener to the deleteButton element
this.elements.deleteButton.addEventListener('click', this.deleteTodoItem.bind(this));
}
// Delete to-do item
deleteTodoItem() {
this.remove();
}
}
customElements.define('todo-item', TodoItem);
In the code above, we are adding an event listener to the deleteButton
element. When the button is clicked, we call the deleteTodoItem
method. In this method, we remove the todo-item
element from the DOM.
If you preview index.html
in your browser now, you should be able to add new to-do items and delete them.
Going (a bit) further
Our custom elements are working, but let’s add some more functionalities to make our to-do list more useful. We will add the ability to mark to-do items as completed by adding a is-completed
class to the todo-item
element when the checkbox is checked. To do this, we will add an event listener to the checkbox
element in the TodoItem
class:
class TodoItem extends HTMLElement {
constructor() {
super();
console.log('Todo item created!');
this.elements = {
checkbox: this.querySelector('[id^="ToDoItem-Checkbox"]'),
label: this.querySelector('[id^="ToDoItem-Label"]'),
deleteButton: this.querySelector('[id^="ToDoItem-DeleteButton"]'),
};
// Add event listener to the deleteButton element
this.elements.deleteButton.addEventListener('click', this.deleteTodoItem.bind(this));
// Add event listener to the checkbox element
this.elements.checkbox.addEventListener('change', this.toggleCompleted.bind(this));
}
// Delete to-do item
deleteTodoItem() {
this.remove();
}
// Toggle completed
toggleCompleted(event) {
if (event.target.checked) {
this.classList.add('is-completed');
} else {
this.classList.remove('is-completed');
}
}
}
customElements.define('todo-item', TodoItem);
In the code above, we are adding an event listener to the checkbox
element. When the checkbox is checked, we call the toggleCompleted
method. In this method, we add the is-completed
class to the todo-item
element if the checkbox is checked, and remove the class if the checkbox is unchecked. You can add some CSS to style the completed to-do items:
.is-completed {
text-decoration: line-through;
color: #ccc;
}
Why not add the ability to count the number of to-do items and the number of completed to-do items? We will add ToDoList-Count
paragraph element to the index.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom HTML elements with JavaScript</title>
<style>
.is-completed {
text-decoration: line-through;
color: #ccc;
}
</style>
</head>
<body>
<h1>Custom HTML elements with JavaScript</h1>
<todo-list>
<div id="ToDoList-Content"></div>
<p id="ToDoList-Count"></p>
<input type="text" id="ToDoList-Input" />
<button id="ToDoList-AddButton">Add</button>
<!-- START defining the todo-item element template -->
<template>
<todo-item>
<input type="checkbox" />
<label></label>
<button>Delete</button>
</todo-item>
</template>
<!-- END defining the todo-item element template -->
</todo-list>
<script src="js/custom-elements.js"></script>
</body>
</html>
Now, let’s make sure our todo-list
counts the number of to-do items and the number of completed to-do items, and updates the text content of the ToDoList-Count
element:
class TodoList extends HTMLElement {
constructor() {
super();
console.log('Todo list created!');
this.elements = {
content: this.querySelector('[id^="ToDoList-Content"]'),
addButton: this.querySelector('[id^="ToDoList-AddButton"]'),
input: this.querySelector('[id^="ToDoList-Input"]'),
count: this.querySelector('[id^="ToDoList-Count"]'),
todoItemTemplate: document.getElementById('[id^="ToDoList-Template-ToDoItem"]'),
};
// Add event listener to the addButton element
this.elements.addButton.addEventListener('click', this.addTodoItem.bind(this));
}
// Add new to-do item
addTodoItem() {
const todoItem = this.elements.todoItemTemplate.content.cloneNode(true);
const label = todoItem.querySelector('[id^="ToDoItem-Label"]');
label.textContent = this.elements.input.value;
this.elements.content.appendChild(todoItem);
this.clearInput();
this.updateCount();
}
// clear input field
clearInput() {
this.elements.input.value = '';
}
// Update count
updateCount() {
const count = this.elements.content.children.length;
const completedCount = this.elements.content.querySelectorAll('.is-completed').length;
this.elements.count.textContent = `Total: ${count}, Completed: ${completedCount}`;
}
}
customElements.define('todo-list', TodoList);
In the code above, we are adding the count
element to the elements
object. We are calling the updateCount
method after adding a new to-do item to update the count. In the updateCount
method, we are getting the total number of to-do items and the number of completed to-do items, and updating the text content of the count
element.
If you preview index.html
in your browser now, you should be able to see the number of to-do items and the number of completed to-do items.
Conclusion
Who says you need a library or a framework to create web components? Okay, you might need them for more complex applications, but for simple ones, you can just use custom HTML elements with JavaScript. In this tutorial, we learned how to create custom HTML elements with JavaScript. We covered the basics of custom elements, how to create them, and how to use them in your projects. However, we can go few steps further by implementing custom events, which would allow us to communicate between custom elements, or by using slots to pass content to custom elements. Also, we could use the Shadow DOM to encapsulate the styles and the structure of our custom elements. But that’s a story for another tutorial :)
I hope you enjoyed this tutorial. If you have any questions or feedback, feel free to let me know. As always, happy coding!