Custom Elements

Web Components start with Custom Elements. A class extending HTMLElement, registered with customElements.define, becomes a new HTML tag the browser understands natively.

May 1, 20264 min read1 / 3

I kept thinking of Web Components as a framework -- something to adopt wholesale or ignore entirely. What changed my thinking was learning that they are three separate browser APIs, each usable on its own. Custom Elements is the first one: a way to register a class as a recognized HTML tag, nothing more.

The Essentials

  1. Custom Elements are registered HTML tags: A class extending HTMLElement, passed to customElements.define, becomes a tag the browser understands. Use <menu-page> in HTML and the browser creates an instance of your class.
  2. The hyphen is mandatory: All custom element names must contain a hyphen (menu-page, not menupage). This prevents future conflicts with standard HTML elements, which the W3C has committed to never defining with a hyphen.
  3. Always call super() in the constructor: Custom elements extend HTMLElement. Skipping super() will break certain element behaviors in unpredictable ways.
  4. Registration happens on import: The customElements.define() call runs when the module executes. Importing the file in app.js is enough to register the element - you do not need to explicitly use the import elsewhere.
  5. Lifecycle callbacks: connectedCallback fires when the element is added to the DOM. disconnectedCallback fires when it is removed. attributeChangedCallback fires when an attribute changes.

Defining a Custom Element

JavaScript
// components/MenuPage.js class MenuPage extends HTMLElement { constructor() { super(); } connectedCallback() { // Runs when the element enters the DOM } } customElements.define('menu-page', MenuPage); export default MenuPage;

The class inherits from HTMLElement - the base interface for all DOM elements. The customElements.define call registers the tag name. After that, anywhere in the document, <menu-page></menu-page> will create an instance of this class.

Registering by Import

The browser does not scan for your JavaScript files. It only runs code it has loaded. To make a custom element available, the module that calls customElements.define must be in the import chain:

JavaScript
// app.js import MenuPage from './components/MenuPage.js'; import DetailsPage from './components/DetailsPage.js'; import OrderPage from './components/OrderPage.js';

Just importing the files is enough. When the browser executes these modules, the customElements.define calls run and the elements become known. From that point on, the router can create them with document.createElement('menu-page').

The Lifecycle

Custom elements have a simpler lifecycle than most frameworks:

constructor - Runs when the element is created. Set up initial state and private properties here. Do not manipulate the DOM inside the constructor - the element has no children and may not be connected to the document yet.

connectedCallback - Runs when the element is inserted into the DOM. This is where you build the element's content: clone a template, fetch data, attach event listeners.

disconnectedCallback - Runs when the element is removed from the DOM. Use this to clean up event listeners or cancel pending requests.

attributeChangedCallback(name, oldValue, newValue) - Runs when one of the element's observed attributes changes. Which attributes trigger it is controlled by a static observedAttributes getter.

JavaScript
class MenuPage extends HTMLElement { static get observedAttributes() { return ['data-category']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'data-category') { this.render(newValue); } } }

Attributes and dataset

HTML attributes only accept strings. If you need to pass a product ID to a <details-page> element, you pass it as a string via a data-* attribute:

JavaScript
const page = document.createElement('details-page'); page.dataset.id = productId; // Sets data-id attribute main.appendChild(page);

Inside the component, connectedCallback reads it back:

JavaScript
connectedCallback() { const id = this.dataset.id; // use id to fetch or look up the product }

This is the same dataset API covered in the routing section. Web components lean on it because HTML-attribute-based communication is the native model.

What Custom Elements Are Not

Custom elements register a class with a tag name. They do not provide:

  • Scoped styles (that is Shadow DOM)
  • Reusable markup (that is HTML templates)
  • Isolated DOM (that is Shadow DOM again)

Each of these is a separate spec. Custom elements work independently of the others. The combination of all three is what the community calls "Web Components," but you can use any piece in isolation.

Further Reading and Watching

Video: