About this specification

This specification is like no other — it has been processed with you, the humble web developer, in mind.

The focus of this specification is readability and ease of access. Unlike the full HTML Standard, this "developer's edition" removes information that only browser vendors need know. It is automatically produced from the full specification by our build tooling, and thus always in sync with the latest developments in HTML.

To read about its conception, construction, and future, read the original press release, and the blog post about its relaunch.

Finally, feel free to contribute on GitHub to make this edition better for everyone!

    1. 4.13 Custom elements
      1. 4.13.1 Introduction
        1. 4.13.1.1 Creating an autonomous custom element
        2. 4.13.1.2 Creating a customized built-in element
        3. 4.13.1.3 Drawbacks of autonomous custom elements
        4. 4.13.1.4 Upgrading elements after their creation
      2. 4.13.2 Requirements for custom element constructors
      3. 4.13.3 Core concepts
      4. 4.13.4 The CustomElementRegistry interface
      5. 4.13.5 Upgrades
      6. 4.13.6 Custom element reactions

4.13 Custom elements

4.13.1 Introduction

Custom elements provide a way for authors to build their own fully-featured DOM elements. Although authors could always use non-standard elements in their documents, with application-specific behavior added after the fact by scripting or similar, such elements have historically been non-conforming and not very functional. By defining a custom element, authors can inform the parser how to properly construct an element and how elements of that class should react to changes.

Custom elements are part of a larger effort to "rationalise the platform", by explaining existing platform features (like the elements of HTML) in terms of lower-level author-exposed extensibility points (like custom element definition). Although today there are many limitations on the capabilities of custom elements—both functionally and semantically—that prevent them from fully explaining the behaviors of HTML's existing elements, we hope to shrink this gap over time.

4.13.1.1 Creating an autonomous custom element

For the purposes of illustrating how to create an autonomous custom element, let's define a custom element that encapsulates rendering a small icon for a country flag. Our goal is to be able to use it like so:

<flag-icon country="nl"></flag-icon>

To do this, we first declare a class for the custom element, extending HTMLElement:

class FlagIcon extends HTMLElement {
  constructor() {
    super();
    this._countryCode = null;
  }

  static get observedAttributes() { return ["country"]; }

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "country" due to observedAttributes
    this._countryCode = newValue;
    this._updateRendering();
  }
  connectedCallback() {
    this._updateRendering();
  }

  get country() {
    return this._countryCode;
  }
  set country(v) {
    this.setAttribute("country", v);
  }

  _updateRendering() {
    // Left as an exercise for the reader. But, you'll probably want to
    // check this.ownerDocument.defaultView to see if we've been
    // inserted into a document with a browsing context, and avoid
    // doing any work if not.
  }
}

We then need to use this class to define the element:

customElements.define("flag-icon", FlagIcon);

At this point, our above code will work! The parser, whenever it sees the flag-icon tag, will construct a new instance of our FlagIcon class, and tell our code about its new country attribute, which we then use to set the element's internal state and update its rendering (when appropriate).

You can also create flag-icon elements using the DOM API:

const flagIcon = document.createElement("flag-icon")
flagIcon.country = "jp"
document.body.appendChild(flagIcon)

Finally, we can also use the custom element constructor itself. That is, the above code is equivalent to:

const flagIcon = new FlagIcon()
flagIcon.country = "jp"
document.body.appendChild(flagIcon)
4.13.1.2 Creating a customized built-in element

Customized built-in elements are a distinct kind of custom element, which are defined slightly differently and used very differently compared to autonomous custom elements. They exist to allow reuse of behaviors from the existing elements of HTML, by extending those elements with new custom functionality. This is important since many of the existing behaviors of HTML elements can unfortunately not be duplicated by using purely autonomous custom elements. Instead, customized built-in elements allow the installation of custom construction behavior, lifecycle hooks, and prototype chain onto existing elements, essentially "mixing in" these capabilities on top of the already-existing element.

Customized built-in elements require a distinct syntax from autonomous custom elements because user agents and other software key off an element's local name in order to identify the element's semantics and behavior. That is, the concept of customized built-in elements building on top of existing behavior depends crucially on the extended elements retaining their original local name.

In this example, we'll be creating a customized built-in element named plastic-button, which behaves like a normal button but gets fancy animation effects added whenever you click on it. We start by defining a class, just like before, although this time we extend HTMLButtonElement instead of HTMLElement:

class PlasticButton extends HTMLButtonElement {
  constructor() {
    super();

    this.addEventListener("click", () => {
      // Draw some fancy animation effects!
    });
  }
}

When defining our custom element, we have to also specify the extends option:

customElements.define("plastic-button", PlasticButton, { extends: "button" });

In general, the name of the element being extended cannot be determined simply by looking at what element interface it extends, as many elements share the same interface (such as q and blockquote both sharing HTMLQuoteElement).

To use our customized built-in element, we use the is attribute on a button element:

<button is="plastic-button">Click Me!</button>

Trying to use a customized built-in element as an autonomous custom element will not work; that is, <plastic-button>Click me?</plastic-button> will simply create an HTMLElement with no special behavior.

If you need to create a type-extended element programmatically, you can use the following form of createElement():

const plasticButton = document.createElement("button", { is: "plastic-button" });
plasticButton.textContent = "Click me!";

And as before, the constructor will also work:

const plasticButton2 = new PlasticButton();
console.log(plasticButton2.localName);          // will output "button"
console.log(plasticButton2.getAttribute("is")); // will output "plastic-button"

Notably, all the of the ways in which button is special apply to such "plastic buttons" as well: their focus behavior, ability to participate in form submission, the disabled attribute, and so on.

Customized built-in elements are designed to allow extension of existing HTML elements that have useful user-agent supplied behavior or APIs. As such, they can only extend existing HTML elements defined in this specification, and cannot extend legacy elements such as bgsound, blink, isindex, keygen, multicol, nextid, or spacer that have been defined to use HTMLUnknownElement as their element interface.

One reason for this requirement is future-compatibility: if a customized built-in element was defined that extended a currently-unknown element, for example combobox, this would prevent this specification from defining a combobox element in the future, as consumers of the derived customized built-in element would have come to depend on their base element having no interesting user-agent-supplied behavior.

Additionally, applet elements cannot be extended, as they are in the process of being removed from the Web platform.

4.13.1.3 Drawbacks of autonomous custom elements

As specified below, and alluded to above, simply defining and using an element called taco-button does not mean that such elements represent buttons. That is, tools such as Web browsers, search engines, or accessibility technology will not automatically treat the resulting element as a button just based on its defined name.

To convey the desired button semantics to a variety of users, while still using an autonomous custom element, a number of techniques would need to be employed:

With these points in mind, a full-featured taco-button that took on the responsibility of conveying button semantics (including the ability to be disabled) might look something like this:

class TacoButton extends HTMLElement {
  static get observedAttributes() { return ["disabled"]; }

  constructor() {
    super();

    this.addEventListener("keydown", e => {
      if (e.keyCode === 32 || e.keyCode === 13) {
        this.dispatchEvent(new MouseEvent("click", {
          bubbles: true,
          cancelable: true
        }));
      }
    });

    this.addEventListener("click", e => {
      if (this.disabled) {
        e.preventDefault();
        e.stopPropagation();
      }
    });

    this._observer = new MutationObserver(() => {
      this.setAttribute("aria-label", this.textContent);
    });
  }

  connectedCallback() {
    this.setAttribute("role", "button");
    this.setAttribute("tabindex", "0");

    this._observer.observe(this, {
      childList: true,
      characterData: true,
      subtree: true
    });
  }

  disconnectedCallback() {
    this._observer.disconnect();
  }

  get disabled() {
    return this.hasAttribute("disabled");
  }

  set disabled(v) {
    if (v) {
      this.setAttribute("disabled", "");
    } else {
      this.removeAttribute("disabled");
    }
  }

  attributeChangedCallback() {
    // only is called for the disabled attribute due to observedAttributes
    if (this.disabled) {
      this.removeAttribute("tabindex");
      this.setAttribute("aria-disabled", "true");
    } else {
      this.setAttribute("tabindex", "0");
      this.setAttribute("aria-disabled", "false");
    }
  }
}

Even with this rather-complicated element definition, the element is not a pleasure to use for consumers: it will be continually "sprouting" tabindex and aria-* attributes of its own volition. This is because as of now there is no way to specify default accessibility semantics or focus behavior for custom elements, forcing the use of these attributes to do so (even though they are usually reserved for allowing the consumer to override default behavior).

In contrast, a simple customized built-in element, as shown in the previous section, would automatically inherit the semantics and behavior of the button element, with no need to implement these behaviors manually. In general, for any elements with nontrivial behavior and semantics that build on top of existing elements of HTML, customized built-in elements will be easier to develop, maintain, and consume.

4.13.1.4 Upgrading elements after their creation

Because element definition can occur at any time, a non-custom element could be created, and then later become a custom element after an appropriate definition is registered. We call this process "upgrading" the element, from a normal element into a custom element.

Upgrades enable scenarios where it may be preferable for custom element definitions to be registered after relevant elements has been initially created, such as by the parser. They allow progressive enhancement of the content in the custom element. For example, in the following HTML document the element definition for img-viewer is loaded asynchronously:

<!DOCTYPE html>
<html lang="en">
<title>Image viewer example</title>

<img-viewer filter="Kelvin">
  <img src="images/tree.jpg" alt="A beautiful tree towering over an empty savannah">
</img-viewer>

<script src="js/elements/img-viewer.js" async></script>

The definition for the img-viewer element here is loaded using a script element marked with the async attribute, placed after the <img-viewer> tag in the markup. While the script is loading, the img-viewer element will be treated as an undefined element, similar to a span. Once the script loads, it will define the img-viewer element, and the existing img-viewer element on the page will be upgraded, applying the custom element's definition (which presumably includes applying an image filter identified by the string "Kelvin", enhancing the image's visual appearance).


Note that upgrades only apply to elements in the document tree. (Formally, elements that are connected.) An element that is not inserted into a document will stay un-upgraded. An example illustrates this point:

<!DOCTYPE html>
<html lang="en">
<title>Upgrade edge-cases example</title>

<example-element></example-element>

<script>
  "use strict";

  const inDocument = document.querySelector("example-element");
  const outOfDocument = document.createElement("example-element");

  // Before the element definition, both are HTMLElement:
  console.assert(inDocument instanceof HTMLElement);
  console.assert(outOfDocument instanceof HTMLElement);

  class ExampleElement extends HTMLElement {}
  customElements.define("example-element", ExampleElement);

  // After element definition, the in-document element was upgraded:
  console.assert(inDocument instanceof ExampleElement);
  console.assert(!(outOfDocument instanceof ExampleElement));

  document.body.appendChild(outOfDocument);

  // Now that we've moved the element into the document, it too was upgraded:
  console.assert(outOfDocument instanceof ExampleElement);
</script>

4.13.2 Requirements for custom element constructors

When authoring custom element constructors, authors are bound by the following conformance requirements:

Several of these requirements are checked during element creation, either directly or indirectly, and failing to follow them will result in a custom element that cannot be instantiated by the parser or DOM APIs. This is true even if the work is done inside a constructor-initiated microtask, as a microtask checkpoint can occur immediately after construction.

4.13.3 Core concepts

A custom element is an element that is custom. Informally, this means that its constructor and prototype are defined by the author, instead of by the user agent. This author-supplied constructor function is called the custom element constructor.

Two distinct types of custom elements can be defined:

  1. An autonomous custom element, which is defined with no extends option. These types of custom elements have a local name equal to their defined name.

  2. A customized built-in element, which is defined with an extends option. These types of custom elements have a local name equal to the value passed in their extends option, and their defined name is used as the value of the is attribute, which therefore must be a valid custom element name.

After a custom element is created, changing the value of the is attribute does not change the element's behavior, as it is saved on the element as its is value.

Autonomous custom elements have the following element definition:

Categories:
Flow content.
Phrasing content.
Palpable content.
Contexts in which this element can be used:
Where phrasing content is expected.
Content model:
Transparent.
Content attributes:
Global attributes, except the is attribute
Any other attribute that has no namespace (see prose).
DOM interface:
Supplied by the element's author (inherits from HTMLElement)

An autonomous custom element does not have any special meaning: it represents its children. A customized built-in element inherits the semantics of the element that it extends.

Any namespace-less attribute that is relevant to the element's functioning, as determined by the element's author, may be specified on an autonomous custom element, so long as the attribute name is XML-compatible and contains no ASCII upper alphas. The exception is the is attribute, which must not be specified on an autonomous custom element (and which will have no effect if it is).

Customized built-in elements follow the normal requirements for attributes, based on the elements they extend. To add custom attribute-based behavior, use data-* attributes.


A valid custom element name is a sequence of characters name that meets all of the following requirements:

These requirements ensure a number of goals for valid custom element names:

Apart from these restrictions, a large variety of names is allowed, to give maximum flexibility for use cases like <math-α> or <emotion-😍>.

A custom element definition describes a custom element and consists of:

A name
A valid custom element name
A local name
A local name
A constructor
A Web IDL Function callback function type value wrapping the custom element constructor
A list of observed attributes
A sequence<DOMString>
A collection of lifecycle callbacks
A map, whose four keys are the strings "connectedCallback", "disconnectedCallback", "adoptedCallback", and "attributeChangedCallback". The corresponding values are either a Web IDL Function callback function type value, or null. By default the value of each entry is null.
A construction stack
A list, initially empty, that is manipulated by the upgrade an element algorithm and the HTML element constructors. Each entry in the list will be either an element or an already constructed marker.

To look up a custom element definition, given a document, namespace, localName, and is, perform the following steps. They will return either a custom element definition or null:

  1. If namespace is not the HTML namespace, return null.

  2. If document does not have a browsing context, return null.

  3. Let registry be document's browsing context's Window's CustomElementRegistry object.

  4. If there is custom element definition in registry with name and local name both equal to localName, return that custom element definition.

  5. If there is a custom element definition in registry with name equal to is and local name equal to localName, return that custom element definition.

  6. Return null.

4.13.4 The CustomElementRegistry interface

Each Window object is associated with a unique instance of a CustomElementRegistry object, allocated when the Window object is created.

Custom element registries are associated with Window objects, instead of Document objects, since each custom element constructor inherits from the HTMLElement interface, and there is exactly one HTMLElement interface per Window object.

The customElements attribute of the Window interface must return the CustomElementRegistry object for that Window object.

Every CustomElementRegistry has a set of custom element definitions, initially empty. In general, algorithms in this specification look up elements in the registry by any of name, local name, or constructor.

Every CustomElementRegistry also has an element definition is running flag which is used to prevent reentrant invocations of element definition. It is initially unset.

Every CustomElementRegistry also has a when-defined promise map, mapping valid custom element names to promises. It is used to implement the whenDefined() method.

window . customElements . define(name, constructor)
Defines a new custom element, mapping the given name to the given constructor as an autonomous custom element.
window . customElements . define(name, constructor, { extends: baseLocalName })
Defines a new custom element, mapping the given name to the given constructor as a customized built-in element for the element type identified by the supplied baseLocalName. A "NotSupportedError" DOMException will be thrown upon trying to extend a custom element or an unknown element.
window . customElements . get(name)
Retrieves the custom element constructor defined for the given name. Returns undefined if there is no custom element definition with the given name.
window . customElements . whenDefined(name)
Returns a promise that will be fulfilled when a custom element becomes defined with the given name. (If such a custom element is already defined, the returned promise will be immediately fulfilled.) Returns a promise rejected with a "SyntaxError" DOMException if not given a valid custom element name.

Element definition is a process of adding a custom element definition to the CustomElementRegistry. This is accomplished by the define() method. When invoked, the define(name, constructor, options) method must run these steps:

  1. If IsConstructor(constructor) is false, then throw a TypeError and abort these steps.

  2. If name is not a valid custom element name, then throw a "SyntaxError" DOMException and abort these steps.

  3. If this CustomElementRegistry contains an entry with name name, then throw a "NotSupportedError" DOMException and abort these steps.

  4. If this CustomElementRegistry contains an entry with constructor constructor, then throw a "NotSupportedError" DOMException and abort these steps.

  5. Let localName be name.

  6. Let extends be the value of the extends member of options, or null if no such member exists.

  7. If extends is not null, then:

    1. If extends is a valid custom element name, then throw a "NotSupportedError" DOMException.

    2. If the element interface for extends and the HTML namespace is HTMLUnknownElement (e.g., if extends does not indicate an element definition in this specification), then throw a "NotSupportedError" DOMException.

    3. Set localName to extends.

  8. If this CustomElementRegistry's element definition is running flag is set, then throw a "NotSupportedError" DOMException and abort these steps.

  9. Set this CustomElementRegistry's element definition is running flag.

  10. Run the following substeps while catching any exceptions:

    1. Let prototype be Get(constructor, "prototype"). Rethrow any exceptions.

    2. If Type(prototype) is not Object, then throw a TypeError exception.

    3. Let lifecycleCallbacks be a map with the four keys "connectedCallback", "disconnectedCallback", "adoptedCallback", and "attributeChangedCallback", each of which belongs to an entry whose value is null.

    4. For each of the four keys callbackName in lifecycleCallbacks, in the order listed in the previous step:

      1. Let callbackValue be Get(prototype, callbackName). Rethrow any exceptions.

      2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of converting callbackValue to the Web IDL Function callback type. Rethrow any exceptions from the conversion.

    5. Let observedAttributes be an empty sequence<DOMString>.

    6. If the value of the entry in lifecycleCallbacks with key "attributeChangedCallback" is not null, then:

      1. Let observedAttributesIterable be Get(constructor, "observedAttributes"). Rethrow any exceptions.

      2. If observedAttributesIterable is not undefined, then set observedAttributes to the result of converting observedAttributesIterable to a sequence<DOMString>. Rethrow any exceptions from the conversion.

    Then, perform the following substep, regardless of whether the above steps threw an exception or not:

    1. Unset this CustomElementRegistry's element definition is running flag.

    Finally, if the first set of substeps threw an exception, then rethrow that exception, and terminate this algorithm. Otherwise, continue onward.

  11. Let definition be a new custom element definition with name name, local name localName, constructor constructor, observed attributes observedAttributes, and lifecycle callbacks lifecycleCallbacks.

  12. Add definition to this CustomElementRegistry.

  13. Let document be this CustomElementRegistry's relevant global object's associated Document.

  14. Let upgrade candidates be all elements that are shadow-including descendants of document, whose namespace is the HTML namespace and whose local name is localName, in shadow-including tree order. Additionally, if extends is non-null, only include elements whose is value is equal to name.

  15. For each element element in upgrade candidates, enqueue a custom element upgrade reaction given element and definition.

  16. If this CustomElementRegistry's when-defined promise map contains an entry with key name:

    1. Let promise be the value of that entry.

    2. Resolve promise with undefined.

    3. Delete the entry with key name from this CustomElementRegistry's when-defined promise map.

When invoked, the get(name) method must run these steps:

  1. If this CustomElementRegistry contains an entry with name name, then return that entry's constructor.

  2. Otherwise, return undefined.

When invoked, the whenDefined(name) method must run these steps:

  1. If name is not a valid custom element name, then return a new promise rejected with a "SyntaxError" DOMException and abort these steps.

  2. If this CustomElementRegistry contains an entry with name name, then return a new promise resolved with undefined and abort these steps.

  3. Let map be this CustomElementRegistry's when-defined promise map.

  4. If map does not contain an entry with key name, create an entry in map with key name and whose value is a new promise.

  5. Let promise be the value of the entry in map with key name.

  6. Return promise.

The whenDefined() method can be used to avoid performing an action until all appropriate custom elements are defined. In this example, we combine it with the :defined pseudo-class to hide a dynamically-loaded article's contents until we're sure that all of the autonomous custom elements it uses are defined.

articleContainer.hidden = true;

fetch(articleURL)
  .then(response => response.text())
  .then(text => {
    articleContainer.innerHTML = text;

    return Promise.all(
      [...articleContainer.querySelectorAll(":not(:defined)")]
        .map(el => customElements.whenDefined(el.localName))
    );
  })
  .then(() => {
    articleContainer.hidden = false;
  });

4.13.5 Upgrades

To upgrade an element, given as input a custom element definition definition and an element element, run the following steps:

  1. If element is custom, abort these steps.

    This can occur due to reentrant invocation of this algorithm, as in the following example:

    <!DOCTYPE html>
    <x-foo id="a"></x-foo>
    <x-foo id="b"></x-foo>
    
    <script>
    // Defining enqueues upgrade reactions for both "a" and "b"
    customElements.define("x-foo", class extends HTMLElement {
      constructor() {
        super();
    
        const b = document.querySelector("#b");
        b.remove();
    
        // While this constructor is running for "a", "b" is still
        // undefined, and so inserting it into the document will enqueue a
        // second upgrade reaction for "b" in addition to the one enqueued
        // by defining x-foo.
        document.body.appendChild(b);
      }
    })
    </script>

    This step will thus bail out the algorithm early when upgrade an element is invoked with "b" a second time.

  2. If element's custom element state is "failed", then abort these steps.

  3. For each attribute in element's attribute list, in order, enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attribute's local name, null, attribute's value, and attribute's namespace.

  4. If element is connected, then enqueue a custom element callback reaction with element, callback name "connectedCallback", and an empty argument list.

  5. Add element to the end of definition's construction stack.

  6. Let C be definition's constructor.

  7. Run the following substeps while catching any exceptions:

    1. Let constructResult be the result of constructing C, with no arguments.

      If C non-conformantly uses an API decorated with the [CEReactions] extended attribute, then the reactions enqueued at the beginning of this algorithm will execute during this step, before C finishes and control returns to this algorithm. Otherwise, they will execute after C and the rest of the upgrade process finishes.

    2. If SameValue(constructResult, element) is false, then throw an "InvalidStateError" DOMException.

      This can occur if C constructs another instance of the same custom element before calling super(), or if C uses JavaScript's return-override feature to return an arbitrary object from the constructor.

    Then, perform the following substep, regardless of whether the above steps threw an exception or not:

    1. Remove the last entry from the end of definition's construction stack.

      Assuming C calls super() (as it will if it is conformant), and that the call succeeds, this will be the already constructed marker that replaced the element we pushed at the beginning of this algorithm. (The HTML element constructor carries out this replacement.)

      If C does not call super() (i.e. it is not conformant), or if any step in the HTML element constructor throws, then this entry will still be element.

    Finally, if the above steps threw an exception, then:

    1. Set element's custom element state to "failed".

    2. Empty element's custom element reaction queue.

    3. Rethrow the exception, and terminate this algorithm.

  8. Set element's custom element state to "custom".

  9. Set element's custom element definition to definition.

To try to upgrade an element, given as input an element element, run the following steps:

  1. Let definition be the result of looking up a custom element definition given element's node document, element's namespace, element's local name, and element's is value.

  2. If definition is not null, then enqueue a custom element upgrade reaction given element and definition.

4.13.6 Custom element reactions

A custom element possesses the ability to respond to certain occurrences by running author code:

We call these reactions collectively custom element reactions.

The way in which custom element reactions are invoked is done with special care, to avoid running author code during the middle of delicate operations. Effectively, they are delayed until "just before returning to user script". This means that for most purposes they appear to execute synchronously, but in the case of complicated composite operations (like cloning, or range manipulation), they will instead be delayed until after all the relevant user agent processing steps have completed, and then run together as a batch.

Additionally, the precise ordering of these reactions is managed via a somewhat-complicated stack-of-queues system, described below. The intention behind this system is to guarantee that custom element reactions always are invoked in the same order as their triggering actions, at least within the local context of a single custom element. (Because custom element reaction code can perform its own mutations, it is not possible to give a global ordering guarantee across multiple elements.)


Each unit of related similar-origin browsing contexts has a custom element reactions stack, which is initially empty. The current element queue is the element queue at the top of the custom element reactions stack. Each item in the stack is an element queue, which is initially empty as well. Each item in an element queue is an element. (The elements are not necessarily custom yet, since this queue is used for upgrades as well.)

Each custom element reactions stack has an associated backup element queue, which an initially-empty element queue. Elements are pushed onto the backup element queue during operations that affect the DOM without going through an API decorated with [CEReactions], or through the parser's create an element for the token algorithm. An example of this is a user-initiated editing operation which modifies the descendants or attributes of an editable element. To prevent reentrancy when processing the backup element queue, each custom element reactions stack also has a processing the backup element queue flag, initially unset.

All elements have an associated custom element reaction queue, initially empty. Each item in the custom element reaction queue is of one of two types:

This is all summarized in the following schematic diagram:

A custom element reactions stack consists of a stack of element queues. Zooming in on a particular queue, we see that it contains a number of elements (in our example, <x-a>, then <x-b>, then <x-c>). Any particular element in the queue then has a custom element reaction queue. Zooming in on the custom element reaction queue, we see that it contains a variety of queued-up reactions (in our example, upgrade, then attribute changed, then another attribute changed, then connected).

To enqueue an element on the appropriate element queue, given an element element, run the following steps:

  1. If the custom element reactions stack is empty, then:

    1. Add element to the backup element queue.

    2. If the processing the backup element queue flag is set, abort this algorithm.

    3. Set the processing the backup element queue flag.

    4. Queue a microtask to perform the following steps:

      1. Invoke custom element reactions in the backup element queue.

      2. Unset the processing the backup element queue flag.

  2. Otherwise, add element to the current element queue.

To enqueue a custom element callback reaction, given a custom element element, a callback name callbackName, and a list of arguments args, run the following steps:

  1. Let definition be element's custom element definition.

  2. Let callback be the value of the entry in definition's lifecycle callbacks with key callbackName.

  3. If callback is null, then abort these steps.

  4. If callbackName is "attributeChangedCallback", then:

    1. Let attributeName be the first element of args.

    2. If definition's observed attributes does not contain attributeName, then abort these steps.

  5. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.

  6. Enqueue an element on the appropriate element queue given element.

To enqueue a custom element upgrade reaction, given an element element and custom element definition definition, run the following steps:

  1. Add a new upgrade reaction to element's custom element reaction queue, with custom element definition definition.

  2. Enqueue an element on the appropriate element queue given element.

To invoke custom element reactions in an element queue queue, run the following steps:

  1. For each custom element element in queue:

    1. Let reactions be element's custom element reaction queue.

    2. Repeat until reactions is empty:

      1. Remove the first element of reactions, and let reaction be that element. Switch on reaction's type:

        upgrade reaction
        Upgrade element using reaction's custom element definition.
        callback reaction
        Invoke reaction's callback function with reaction's arguments, and with element as the callback this value.

        If this throws an exception, catch it, and report the exception.


To ensure custom element reactions are triggered appropriately, we introduce the [CEReactions] IDL extended attribute. It indicates that the relevant algorithm is to be supplemented with additional steps in order to appropriately track and invoke custom element reactions.

The [CEReactions] extended attribute must take no arguments, and must not appear on anything other than an operation, attribute, setter, or deleter. Additionally, it must not appear on readonly attributes.

Operations, attributes, setters, or deleters annotated with the [CEReactions] extended attribute must run the following steps surrounding the actions listed in the description of the operation, setter, deleter, or the attribute's setter:

Before executing the listed actions
Push a new element queue onto the custom element reactions stack.
After executing the listed actions
Pop the element queue from the custom element reactions stack, and invoke custom element reactions in that queue.

The intent behind this extended attribute is somewhat subtle. One way of accomplishing its goals would be to say that every operation, attribute, setter, and deleter on the platform must have these steps inserted, and to allow implementers to optimize away unnecessary cases (where no DOM mutation is possible that could cause custom element reactions to occur).

However, in practice this imprecision could lead to non-interoperable implementations of custom element reactions, as some implementations might forget to invoke these steps in some cases. Instead, we settled on the approach of explicitly annotating all relevant IDL constructs, as a way of ensuring interoperable behavior and helping implementations easily pinpoint all cases where these steps are necessary.

Any nonstandard APIs introduced by the user agent that could modify the DOM in such a way as to cause enqueuing a custom element callback reaction or enqueuing a custom element upgrade reaction, for example by modifying any attributes or child elements, must also be decorated with the [CEReactions] attribute.

As of the time of this writing, the following nonstandard or not-yet-standardized APIs are known to fall into this category: