Web Components Guide

Defining a Component

Most components you write will need some kind of JavaScript. While it's not strictly necessary, more often than not you'll want to add JavaScript to drive logic. To do this you'll need to create a JavaScript class, and use the Custom Elements Registry to attach your class so the browser knows to use it.

Without the Custom Element Registry the browser won't know what JavaScript to associate to what elements. By default, whenever the browser encounters a tag it does not know, it will use the HTMLUnknownElement class to give it a default behavior. You can tell the browser to use a different class by defining the tag name in the Custom Element Registry. With your own class defined, any time the browser sees the defined tag, it will set it up using the associated class.

To define a Custom Element, you can use the global customElements API. You won't need to include any JavaScript libraries to use customElements, it's a global that already exists, like console or localStorage. You can define two types of element:

Autonomous Custom Elements

Autonomous Custom Elements is a fancy way of saying that you're extending from the base element. The base element - HTMLElement - doesn't have a tag, so you need to make one up. It also doesn't have any built in semantics, accessibility, or styling. In that way it's kind of like a <div> or <span> element. How it behaves beyond that is totally up to you.

To define an Autonomous Custom Element, you can call customElements.define giving it a tag name and a class to use. The class has to extend from HTMLElement. Here's an example:

  "my-element", // The tag name
  class extends HTMLElement {} // The class definition

Now, whenever <my-element> appears in HTML, the browser will use that class for the element. The class doesn't do anything on its own but you can add methods or use the lifecycle callbacks to make it do fun things!

If you don't extend from HTMLElement, when your tag is created then the browser will throw a TypeError with a message like autonomous custom elements must extend HTMLElement.

Customized Built-in Elements

Customized Built-in elements are extensions to the browsers existing built-in elements. For example if you wanted to make a button that extends the normal behaviors, you can customize it with a Customized Built-in. Instead of making up your own tag name, you'll use the same tag as the built-in you're targeting. Your class will also have to extend from the existing built-in's class. For example extending the <button> element means your class will need to extends HTMLButtonElement. When you call customElements.define you will need to tell it that you're extending a built-in tag:

  "fancy-button", // The name
  class extends HTMLButtonElement {}, // The class definition
  { extends: "button" } // Only extend "button" elements

To create one of these elements, you'll use the regular tag name (e.g. button) and pass an is= attribute with your element name to tell the browser to use your class. In HTML this looks like:

<button is="fancy-button"></button>

If you're using the DOM APIs, you can use createElement with the is option:

document.createElement("button", { is: "fancy-button" })

If you don't extend from the right HTML*Element class, when your tag is created the browser will throw a TypeError with a message like localName does not match the HTML element interface.

For a full list of the browsers _built-in_ elements and the classes you have to extend from, see here:
Element Tag Name Class to extend from
Anchor <a> HTMLAnchorElement
Area <area> HTMLAreaElement
Audio <audio> HTMLAudioElement
Base <base> HTMLBaseElement
Block Quote <blockquote> HTMLQuoteElement
Body <body> HTMLBodyElement
BR <br> HTMLBRElement
Button <button> HTMLButtonElement
Canvas <canvas> HTMLCanvasElement
Data <data> HTMLDataElement
Data List <datalist> HTMLDataListElement
Del <del> HTMLModElement
Details <details> HTMLDetailsElement
Dialog <dialog> HTMLDialogElement
Div <div> HTMLDivElement
Definition List <dl> HTMLDListElement
Embed <embed> HTMLEmbedElement
Field Set <fieldset> HTMLFieldSetElement
Form <form> HTMLFormElement
H1 <h1> HTMLHeadingElement
H2 <h2> HTMLHeadingElement
H3 <h3> HTMLHeadingElement
H4 <h4> HTMLHeadingElement
H5 <h5> HTMLHeadingElement
H6 <h6> HTMLHeadingElement
HR <hr> HTMLHRElement
Head <head> HTMLHeadElement
HTML <html> HTMLHtmlElement
IFrame <iframe> HTMLIFrameElement
Image <img> HTMLImageElement
Ins <ins> HTMLModElement
Input <input> HTMLInputElement
Label <label> HTMLLabelElement
Legend <legend> HTMLLegendElement
LI <li> HTMLLIElement
Link <link> HTMLLinkElement
Map <map> HTMLMapElement
Menu <menu> HTMLMenuElement
Meta <meta> HTMLMetaElement
Meter <meter> HTMLMeterElement
Object <object> HTMLObjectElement
OList <ol> HTMLOListElement
OptGroup <optgroup> HTMLOptGroupElement
Option <option> HTMLOptionElement
Output <output> HTMLOutputElement
Paragraph <p> HTMLParagraphElement
Picture <picture> HTMLPictureElement
Pre <pre> HTMLPreElement
Progress <progress> HTMLProgressElement
Quote <q> HTMLQuoteElement
Script <script> HTMLScriptElement
Select <select> HTMLSelectElement
Slot <slot> HTMLSlotElement
Source <source> HTMLSourceElement
Span <span> HTMLSpanElement
Style <style> HTMLStyleElement
TableCaption <caption> HTMLTableCaptionElement
TableCell <td> HTMLTableCellElement
Table <table> HTMLTableElement
TableRow <tr> HTMLTableRowElement
TBody <tbody> HTMLTableSectionElement
Template <template> HTMLTemplateElement
TextArea <textarea> HTMLTextAreaElement
Time <time> HTMLTimeElement
Title <title> HTMLTitleElement
Track <track> HTMLTrackElement
UList <ul> HTMLUListElement
Video <video> HTMLVideoElement

Some advanced tricks for defining elements

Depending on how your code is loaded, you might find it runs multiple times. Calling customElements.define on an already defined component will cause an error in the browser:

DOMException: NotSupportedError

If you wanted to guard against re-defining an element you could wrap the call to customElements.define by first checking if it's already been defined with customElements.get:

if (!customElements.get("my-element")) {
  customElements.define("my-element", class extends HTMLElement {})

Another thing you could do is move the definition into a static method on the class, like so:

class MyElement extends HTMLElement {
  static define() {
    customElements.define("my-element", this)

This way users of your component can call MyElement.define() in a place in their code where all components get registered. To make your component even more flexible, you can make it so the tag name can be customized:

class MyElement extends HTMLElement {
  // call MyElement.define() for default tag name
  // or MyElement.define('custom-tag') to define with a custom tag.
  static define(tagName = "my-element") {
    customElements.define(tagName, this)