Awesome
All about HTML Custom Elements
Custom Elements is a WHATWG HTML specification that
provides a mechanism for defining new behaviors (such as dynamic
content or interactivity) for HTML elements with custom names.
Custom elements are just HTML elements, with all of the methods and
properties of other, built-in elements. The only real constraint
is that
custom element names must contain a hyphen (-
).
<message-element>Hi!</message-element>
<super-section>
<p>Custom elements can contain content!</p>
<message-element>And other custom elements!</message-element>
</super-section>
Note: The adopted custom element spec, formerly known
as "v1", differs almost entirely from the original "v0" spec.
If you've been using document.registerElement()
from the v0 API,
then read on to see what's changed.
Table of Contents
- Why custom elements?
- How do they work?
- Browser support
- Customized built-in elements
- Observed attributes
- Polyfills
- Further Reading
Why Custom Elements?
-
Encapsulation. Custom element names avoid ambiguity in markup (versus, say, a
<div>
or<span>
with a "special" class), and provide a solid foundation for scoped styles.-
If you've ever felt like it was wrong to define reuseable components with "special" class names initialized by jQuery selections that can only be called after the DOM is ready (or rely on mutation observers to watch for new instances), then custom elements could be your new jam.
-
In native implementations (see browser support), you can target custom elements before (
:unresolved
in the v0 spec) and after (:defined
in the v1 spec) they've been registered via JavaScript.
-
-
The DOM is the API. Components built with tools like jQuery can be cumbersome to build, modify, and maintain because they often introduce another layer of abstraction (such as the
jQuery
object and its API) on top of the DOM. And while there's nothing to stop one from building custom elements that use jQuery, D3, React, or whatever under the hood, I've found custom elements made with vanilla JS to be easier to grok and read.Put another way: The DOM isn't going away any time soon, and custom elements provide a solid conceptual and technical foundation on which all sorts of amazing things can be built.
-
Web Standards. Custom elements are an adopted WHATWG HTML specification. That means that—in theory, at least—they will eventually be implemented natively in all modern browsers with the same API.
Keep in mind is that custom elements are not mutually exclusive of other web component technologies. In fact, I see them as a powerful force multiplier of any technology that leverages them: When designed well, custom elements put their power into the hands of anyone who can write HTML. Great React components, for instance, can be made even greater by packaging them up as custom elements.
How do they work?
Custom element behaviors are added at runtime (whenever the "registration" occurs in JavaScript), and can hook into a number of different element lifecycle events:
- When an instance of the custom element is created, either via
the class constructor or
document.createElement()
; or when existing custom elements are registered. - When an instance is "connected" to the document, either directly or indirectly. This may be called multiple times, and is generally the best place to add event handlers.
- When an instance is removed (or "disconnected") from the document, either directly or indirectly. This may also be called multiple times.
- When an attribute is changed. You can also subscribe to specific observed attributes if you only care about attributes unique to your element, or to implement attribute reflection.
Browser Support
As of summer 2018:
- Chrome has implemented both the v0 spec and the adopted "v1" spec.
- Safari has implemented so-called "autonomous" custom elements, but has declined to support extension of built-in elements.
- Firefox has implemented the spec behind a feature flag.
Regardless of your support targets, you should use a polyfill.
⚠️ When using custom elements—or anything involving JavaScript, for that matter—always design experiences for progressive enhancement, and plan for the possibility that JavaScript isn't enabled or available.
The API
The custom elements API consists mainly of a CustomElementRegistry object that can be used to register class constructors for custom elements by name:
window.customElements.define('element-name', ElementClass);
Where ElementClass
is a class that extends HTMLElement:
// ES2015
class ElementClass extends HTMLElement {
constructor() {
super() // <-- this is required!
this.created()
}
created() {
console.log('hi!', this)
}
}
Custom element classes may implement any of the following lifecycle (instance) methods:
connectedCallback()
is called whenever the element is added to the document, either directly (document.body.appendChild(el)
) or indirectly (as part of a fragment or a DOM tree that's added to the document).disconnectedCallback()
is called whenever the element is removed from the document.attributeChangedCallback(attr, oldValue, newValue)
is called when an observed attribute (see below) is changed.
and the following static (class) properties:
observedAttributes
is an optional array of attribute names for which theattributeChangedCallback()
will be called. If you do not provide this property, the callback will fire for all attributes.
// ES5
class CounterElement extends HTMLElement {
static get observedAttributes() { return ['value'] }
attributeChangedCallback(name, old, value) {
// we can safely ignore name here because 'value' is the only
// observed attribute
this.value = value
}
get value() {
return ('_value' in this)
? this._value
: (this._value = 0)
}
set value(value) {
this._value = value
}
}
Customized built-in elements
⚠️ Warning: Safari does not yet implement this portion of the spec. If you wish to use it, you will need a polyfill.
Custom elements may extend built-in HTML elements with special
semantics or behaviors (such as <button>
or <input>
). Here's how
they work:
-
Register the element with an additional argument indicating which element name it extends:
window.customElements.define('fancy-button', FancyButton, { extends: 'button' })
-
Instantiate the element in HTML with the
is
attribute of the extended built-in set to the name of the custom element:<!-- this: --> <button is="fancy-button">I am fancy</button> <!-- NOT this: --> <fancy-button>I am not fancy</fancy-button>
-
Instantiate the element in JavaScript by passing an additional argument to
document.createElement()
with theis
property set to the name of the custom element:var fancy = document.createElement('button', {is: 'fancy-button'})
Observed Attributes
Observed attributes are attributes that fire the attributeChangedCallback()
lifecycle method. If the custom element class has a (static)
observedAttributes
array, the callback will fire for only the listed
attributes:
// ES2015
class CustomElement extends HTMLElement {
static get observedAttributes() { return ['foo', 'bar']; }
attributeChangedCallback(attr, old, value) {
// `attr` will only ever equal 'foo' or 'bar'
switch (attr) {
case 'foo':
break
case 'bar':
break
}
}
}
Otherwise, the callback will be fired for all attributes.
// ES2015
class CustomElement extends HTMLElement {
attributeChangedCallback(attr, old, value) {
// `attr` could be anything
}
}
📝 When implementing attribute reflection, please observe the W3C API Design Principles.
The Custom elements registry
The CustomElementRegistry object available at window.customElements
has two additional methods for querying its state and responding to when
specific custom elements are registered:
-
customElements.get('element-name')
returns the class constructor of the provided custom element name (orundefined
if it hasn't been defined). -
customElements.whenDefined('element-name')
returns a Promise that resolves if/when the named custom element is defined viacustomElements.define()
.
Gotchas
Custom Events
You can listen for and dispatch custom events in custom
elements. The only bummer is that, even though most modern browsers
support the CustomEvent
constructor, it's missing in all versions of IE
and in older versions of PhantomJS, which is used for lots of "headless"
integration testing. My advice is to include
this polyfill, which falls
back on the native implementation. Here's how you could have your component
"announce" its readiness to the rest of the document, for instance:
var CustomEvent = require('custom-event');
document.registerElement('my-element', {
prototype: Object.create(
HTMLElement.prototype,
{
attachedCallback: {value: function() {
this.dispatchEvent(new CustomEvent('my-element-ready'));
}}
}
)
});
SVG and Namespaces
Because you need a polyfill and namespaces are tricky, it's
basically impossible to reliably extend SVG elements, or any element that
requires an XML namespace. Your best bet is to write a component that wraps
<svg>
elements or creates them at runtime if they don't exist.
Class Definition
One of the trickiest things about custom elements is the magical incantation
for defining element classes that extend HTMLElement
or its subclasses,
especially in "legacy" ES5 environments that don't support the class
keyword
or super()
calls. There are a couple of ways to pull it off:
-
Create an object literal (rather than a proper constructor function) with a
prototype
that extendsHTMLElement.prototype
. The only way to do this in a single expression is to useObject.create()
, which extends the first argument with descriptors in the second. The important thing to note here is that because these are property descriptors, methods must be provided as objects with avalue
property:// ES5 var CustomElement = { prototype: Object.create( HTMLElement.prototype, { // this will NOT work: createdCallback: function() { }, // but this will: createdCallback: {value: function() { }}, // accessors look like this: someValue: { get: function() { /* ... */ }, set: function(value) { /* ... */ } } } ) };
Note: if you need to support older browsers such as IE8 or below, you will also need a polyfill or shim for ES5 standard APIs, such as aight or es5-shim.
-
A variation on the above method uses
Object.create()
but assigns methods directly:// ES5 var CustomElement = { prototype: Object.create(HTMLElement.prototype) }; CustomElement.prototype.someMethod = function(arg) { /* ... */ }; // any accessors not passed to Object.create() can be defined like so. // note that this is *exactly* what Object.create() is doing under the // hood! Object.defineProperties(CustomElement.prototype, { someValue: { get: function() { /* ... */ }, set: function(value) { /* ... */ } } });
-
Use Babel and the custom-element-classes transform. Your
.babelrc
should look something like this:{ "presets": ["env"], "plugins": [ "transform-custom-element-classes" ] }
which should make it possible to write classes like:
class Widget extends HTMLElement { constructor() { super() } } window.customElements.define('widget-element', Widget)
Polyfills
The semi-official webcomponents/custom-elements polyfill is what GitHub uses,
and it provides a bunch of workarounds for the spec rules involving class
constructors and the new
keyword. You should use it, too!
Further reading
- Custom Elements v1: Reusable Web Components (Google) is a great introduction to custom elements.
- Introducing Custom Elements (WebKit) contains some nice implementation tips.
- Using Custom Elements (MDN) is, like pretty much everything else on MDN, a solid reference.
- Custom Elements Everywhere rates popular web frameworks for compatibility with custom elements.
- The WebComponents.org introduction explains how custom elements fit into the broader landscape of native web components and complement technologies like the Shadow DOM and
<template>
elements.