· 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.

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!

Related Posts

Read more notes