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:
customElements.define( "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:
customElements.define( "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:
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) } }