Work with Custom Elements

Third-party web components are web components that you can create using customElements.define(), resulting in custom elements you can reuse in an LWC app. You don’t need to use custom elements with LWC unless you’re working with third-party web components. When you implement a third-party web component in LWC, we recommend that you refer to that component’s documentation for usage information. For the purpose of this article, custom elements and third-party web components are interchangeable.

Lightning Web Security (LWS) must be enabled in the Salesforce org because Lightning Locker doesn’t support third-party web components.

Note

A custom element must follow these characteristics.

  • Define a component class that extends HTMLElement.
  • Register the custom element within the CustomElementRegistry using customElements.define(name, constructor). The name must contain a hyphen and be unique on a page.

Using a <template> tag in your custom element is optional. You can create a <template> tag using document.createElement("template");. You can’t use a nested <template> tag in your LWC HTML template.

To keep your custom element separate and maintainable, you can define and register the custom element in several ways.

  • In the LWC JavaScript file before the LWC component class definition
  • In a separate JavaScript file within the LWC bundle

Define the Custom Element 

Third-party web components contain a custom element definition, which extends the HTMLElement class. The custom element definition describes how to show the element and what to do when the element is added or removed.

The HTML specification states that the constructor is used to set up initial state, default values, event listeners, and a shadow root.

1class MyCustomElement extends HTMLElement {
2  constructor() {
3    super();
4    // Custom element created
5  }
6  connectedCallback() {
7    // Element is added to document
8  }
9  disconnectedCallback() {
10    // Element is removed from the document
11  }
12  static get observedAttributes() {
13    return [
14      // Array of attribute names to monitor for changes
15    ];
16  }
17  attributeChangedCallback(name, oldValue, newValue) {
18    // One of attributes listed above is modified
19  }
20  adoptedCallback() {
21    // Element is moved to a new document
22  }
23}
24// Registers the element
25customElements.define("my-custom-element", MyCustomElement);

The third-party web component behavior is described in lifecycle callbacks, such as in connectedCallback() or disconnectedCallback().

  • constructor()-Called when the custom element is initialized. To establish the prototype chain, the constructor must call super() first and can specify any pre-rendering processes like setting the content of the shadow. The constructor sets up the initial state and default values, and registers event listeners. The constructor also attaches a shadow root to the custom element using this.attachShadow().
  • connectedCallback()-Invoked when the custom element is connected to the DOM.
  • disconnectedCallback()-Invoked when the custom element is disconnected from the DOM.
  • observedAttributes()-Returns an array of attributes to observe.
  • attributeChangedCallback()-Invoked when an attribute is added, removed, or changed. Specify the attributes to observe in observeAttributes().
  • adoptedCallback()-Invoked when the custom element is moved to a new document.

Attach a Shadow Root to A Custom Element 

A custom element attaches a shadow root to its internals using the attachShadow() Web API method, which specifies one of these encapsulation modes-open or closed. Using open mode, third-party custom elements can use appendChild to append HTML tags like <div> and <style> to its shadow root. They can also use adoptedStyleSheets and other Web APIs on the shadow root.

1constructor() {
2  // Always call super first in constructor
3  super();
4  const shadow = this.attachShadow({ mode: "open" });
5  const div = document.createElement("div");
6  shadow.appendChild(div);
7}

The custom element renders like this in the DOM.

1<my-custom-element>
2  #shadow-root (open)
3  |  <div></div>
4</my-custom-element>

In closed mode, JavaScript access to the shadow DOM tree is blocked and shadowRoot returns null. A custom element can append HTML tags using the ShadowRoot.innerHTML property.

To create a custom element in closed mode, save the reference to the shadow root with a different variable, such as shadow or __shadow.

Note

1constructor() {
2  // Always call super first in constructor
3  super();
4  this._shadow = this.attachShadow({ mode: 'closed' });
5  this._shadow.innerHTML = "<div></div>";
6  }

Despite its name, closed mode doesn’t completely block access to a custom element’s shadow root. For example, a custom element can query a tag in a closed shadow root and update its internals.

1constructor() {
2  // Always call super first in constructor
3  super();
4  this._shadow = this.attachShadow({ mode: 'closed' });
5  this._shadow.innerHTML = "<div></div>";
6}
7
8connectedCallback() {
9  this._shadow.querySelector('div').textContent = "Hello closed mode";
10}

The custom element renders like this in the DOM.

1<my-custom-element>
2  #shadow-root (closed)
3  |  <div>Hello closed mode</div>
4</my-custom-element>

For more information, see MDN Web Docs: Element.shadowRoot and the “mode” option.

Define and Register a Custom Element 

To create the internal shadow DOM structure, a custom element can append content using .innerHTML or DOM APIs such as createElement() and appendChild(). This example defines the custom element outside of the LWC component class.

1//myComponent.js
2import { LightningElement } from "lwc";
3
4// Define and register the element
5customElements.define(
6  "my-custom-element",
7  class extends HTMLElement {
8    constructor() {
9      super();
10      this.attachShadow({ mode: "closed" }).innerHTML = "<div>Custom Element Constructor</div>";
11    }
12  },
13);
14
15export default class MyComponent extends LightningElement {
16  greeting = "World";
17}

To work with the custom element’s attributes and children, use connectedCallback() or renderedCallback() instead. For example, if you’re creating elements and setting attributes on them, defer them to one of the lifecycle callbacks and use attributeChangedCallback() to define a callback when the attribute is changed. For more information, see the custom element specification.

Use the Custom Element in LWC 

Use the custom element in your LWC template with the lwc:external directive.

1<!-- myComponent.html -->
2<template>
3  <div class="slds-var-m-around_medium">Hello, {greeting}!</div>
4  <my-custom-element lwc:external></my-custom-element>
5</template>

Import A Custom Element Within the Component Bundle 

If you define and register a custom element in a separate JavaScript file within a component bundle, import the JavaScript file in your LWC JavaScript file. For example, define and register the custom element in a JavaScript file myCustomElement.js in the myComponent component bundle that follows this folder structure.

1main/default/lwc/myComponent
2   ├──myComponent.html
3   ├──myComponent.js
4   ├──myComponent.js-meta.xml
5   └──myCustomElement.js

In the myCustomElement.js file, define and register the custom element.

1// myCustomElement.js
2class MyCustomElement extends HTMLElement {
3  constructor() {
4    super();
5    this.attachShadow({ mode: "closed" }).innerHTML = "<div>Hello Custom Element</div>";
6  }
7}
8
9// Registers the element
10customElements.define("my-custom-element", MyCustomElement);

To import the JavaScript file from your Lightning web component, use the import syntax.

1// myComponent.js
2import { LightningElement } from "lwc";
3import "./myCustomElement";
4
5export default class MyComponent extends LightningElement {}

In the HTML template file, create an instance of your custom element using lwc:external.

1<!-- myComponent.html -->
2<template>
3  <my-custom-element lwc:external></my-custom-element>
4</template>

Example: Create a Custom Element that Increments a Button Label 

Here’s an example to demonstrate the structure of a custom element. The example creates a button that increments the counter on its label when pressed.

1//myCounterButton.js
2import { LightningElement } from "lwc";
3
4class MyCounter extends HTMLElement {
5  count = 0;
6  handler = () => {
7    this.count++;
8    this._shadow.firstElementChild.innerHTML = this.count;
9  };
10  shadow = null;
11  constructor() {
12    super();
13    this._shadow = this.attachShadow({ mode: "closed" });
14    this._shadow.innerHTML = `Button:<button>${this.count}</button>`;
15  }
16  connectedCallback() {
17    this._shadow.firstElementChild.addEventListener("click", this.handler);
18  }
19  disconnectedCallback() {
20    this._shadow.firstElementChild.removeEventListener("click", this.handler);
21  }
22}
23customElements.define("my-counter", MyCounter);
24export default class MyCounterButton extends LightningElement {}

To use the custom element in LWC, add it to your template using the lwc:external directive.

1<!-- myCounterButton.html -->
2<template>
3  <my-counter lwc:external></my-counter>
4</template>

The component renders in the DOM like this. The button label ${this.count} is incremented each time it’s clicked.

1<my-counter-button>
2    <my-counter>
3      #shadow-root (closed)
4      |  "Button:"
5      |  <button>0</button>
6    </my-counter>
7</my-counter-button>

Example: Increment a Button with Attribute Change Callback 

This example is similar to the previous example, but it watches the custom element’s count attribute, using attributeChangedCallback() to define a callback when the attribute is changed.

1//myCounterWithCallback.js
2import { LightningElement } from "lwc";
3
4customElements.define(
5  "my-counter",
6  class MyCounter extends HTMLElement {
7    constructor() {
8      super();
9      this._shadow = this.attachShadow({ mode: "closed" });
10    }
11
12    get count() {
13      return this.getAttribute("count");
14    }
15
16    set count(val) {
17      this.setAttribute("count", val);
18    }
19
20    connectedCallback() {
21      this.renderButton();
22      let btn = this._shadow.querySelector("#btn");
23      btn.addEventListener("click", this.increment.bind(this));
24    }
25
26    static get observedAttributes() {
27      return ["count"];
28    }
29
30    attributeChangedCallback(prop, oldVal, newValue) {
31      if (prop === "count") {
32        this.renderButton();
33        let btn = this._shadow.querySelector("#btn");
34        btn.addEventListener("click", this.increment.bind(this));
35        // do something else
36      }
37    }
38
39    increment() {
40      this.count++;
41    }
42
43    renderButton() {
44      this._shadow.innerHTML = `
45        <button id="btn">${this.count}</button>
46      `;
47    }
48  },
49);
50
51export default class extends LightningElement {}

Add the custom element tag to your LWC template.

1<!-- myCounterWithCallback.html -->
2<template>
3  <my-counter count="0" lwc:external></my-counter>
4</template>

Example: Generate a Square with Random Attributes 

This example is adapted from the MDN lifecycle callbacks article. It uses a <custom-square> custom element that’s defined and registered in a separate customSquare.js JavaScript file within the same myCustomSquare component bundle.

1main/default/lwc/myCustomSquare
2   ├──myCustomSquare.html
3   ├──myCustomSquare.js
4   ├──myCustomSquare.js-meta.xml
5   └──customSquare.js

The customSquare.js file creates a class for the custom element and registers it using customElements.define("custom-square", Square);. It also exports the random function, so that the myCustomSquare component can import and use it.

1// customSquare.js
2class Square extends HTMLElement {
3  // Specify observed attributes so that
4  // attributeChangedCallback will work
5  static get observedAttributes() {
6    return ["color", "size"];
7  }
8
9  constructor() {
10    // Always call super first in constructor
11    super();
12
13    this._shadow = this.attachShadow({ mode: "closed" });
14    this._shadow.innerHTML = `<style></style><div></div>`;
15  }
16
17  connectedCallback() {
18    console.log("Custom square element added to document.");
19    this.updateStyle();
20  }
21
22  disconnectedCallback() {
23    console.log("Custom square element removed from document.");
24  }
25
26  adoptedCallback() {
27    console.log("Custom square element moved to new document.");
28  }
29
30  attributeChangedCallback(name, oldValue, newValue) {
31    console.log("Custom square element attributes changed.");
32    this.updateStyle();
33  }
34
35  updateStyle() {
36    this._shadow.querySelector("style").textContent = `
37      div {
38        width: ${this.getAttribute("size")}px;
39        height: ${this.getAttribute("size")}px;
40        background-color: ${this.getAttribute("color")};
41      }
42    `;
43  }
44}
45
46customElements.define("custom-square", Square);
47
48function random(min, max) {
49  return Math.floor(Math.random() * (max - min + 1) + min);
50}
51
52export { random };

The myCustomSquare.js file contains the button click handlers to add, update, and remove the custom square from the DOM. The JavaScript initializes the custom attributes and updates these attributes on the custom element. It also imports the random function from the customSquare.js file that defines and registers the custom element.

1// myCustomSquare.js
2import { LightningElement, api } from "lwc";
3import { random } from "./customSquare";
4
5export default class MyCustomSquare extends LightningElement {
6  // Initialize square attributes
7  size = 100;
8  color = "red";
9  showSquare = false;
10
11  // Initialize disabled property for buttons
12  buttonDisabled = true;
13
14  handleAdd() {
15    // Add a custom square element to the DOM
16    this.showSquare = true;
17
18    // Enable the buttons for
19    // interacting with the custom square
20    this.buttonDisabled = false;
21  }
22
23  handleUpdate() {
24    // Randomly update square's attributes
25    this.size = random(50, 200);
26    this.color = `rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`;
27  }
28
29  handleRemove() {
30    // Remove the square
31    this.showSquare = false;
32    this.buttonDisabled = true;
33  }
34}

We don’t recommend using JavaScript to manipulate the DOM. It’s better to use HTML directives to write declarative code. This example uses lwc:if to conditionally display the custom element instead of using appendChild and removeChild. Using the conditional directive also triggers the disconnectedCallback and connectedCallback lifecycle callbacks for the custom element.

Important

Add the buttons to the myCustomSquare HTML template file.

1<template>
2  <lightning-card title="ComponentJavaScriptFile" icon-name="custom:custom14">
3    <div class="slds-var-m-around_medium">
4      <lightning-button onclick={handleAdd} label="Add custom-square to DOM"></lightning-button>
5      <lightning-button onclick={handleUpdate} disabled={buttonDisabled} label="Update attributes">
6      </lightning-button>
7      <lightning-button onclick={handleRemove} disabled={buttonDisabled} label="Remove custom-square from DOM">
8      </lightning-button>
9      <template lwc:if={showSquare}>
10        <custom-square lwc:external 
11          size={size}
12          color={color}></custom-square>
13      </template>
14    </div>
15  </lightning-card>
16</template>

See Also

Release Preview

This release is in preview. Features described here don't become generally available until the latest general availability date that Salesforce announces for this release. Before then, and where features are noted as beta, pilot, or developer preview, we can't guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.