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.
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
- Custom Elements are registered HTML tags: A class extending
HTMLElement, passed tocustomElements.define, becomes a tag the browser understands. Use<menu-page>in HTML and the browser creates an instance of your class. - The hyphen is mandatory: All custom element names must contain a hyphen (
menu-page, notmenupage). This prevents future conflicts with standard HTML elements, which the W3C has committed to never defining with a hyphen. - Always call
super()in the constructor: Custom elements extendHTMLElement. Skippingsuper()will break certain element behaviors in unpredictable ways. - Registration happens on import: The
customElements.define()call runs when the module executes. Importing the file inapp.jsis enough to register the element - you do not need to explicitly use the import elsewhere. - Lifecycle callbacks:
connectedCallbackfires when the element is added to the DOM.disconnectedCallbackfires when it is removed.attributeChangedCallbackfires when an attribute changes.
Defining a Custom Element
// 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:
// 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.
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:
const page = document.createElement('details-page');
page.dataset.id = productId; // Sets data-id attribute
main.appendChild(page);Inside the component, connectedCallback reads it back:
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
- MDN: Using custom elements - Full guide to custom element lifecycle, observed attributes, and registration.
- MDN: CustomElementRegistry.define() - Reference for the
customElements.defineAPI.
Video:
- Web Components Crash Course by Traversy Media. Practical walkthrough of custom elements, templates, and Shadow DOM together.
Keep reading