# Third-Party Web Components (Beta)
Using third-party web components in LWC can save you time recreating the same components in LWC. The third-party web component renders to native web components in LWC templates. Although you can use third-party web components in an LWC template file using an iframe or lwc:dom="manual", we recommend rendering them in your template HTML file using lwc:external.
Custom elements are the building blocks of third-party web components. You don't need to use custom elements with LWC unless you're working with third-party web components. When implementing a third-party web component in LWC, we recommend that you refer to the third-party web component documentation for usage information.
Third-party web components can be of various JavaScript frameworks or written in pure vanilla JS. The MDN web components are examples of third-party web components that you can use with LWC. Similarly, webcomponents.org provides a collection of web components that are based on custom elements and shadow DOM standards.
Note
If you're working with third-party JavaScript libraries, see Use Third-Party JavaScript Libraries instead.
For the purpose of this article, custom elements and third-party web components are interchangeable.
A custom element must follow these characteristics.
- Define a component class that extends
HTMLElement. - Register the custom element within the
CustomElementRegistryusingcustomElements.define(name, constructor). Thenamemust contain a hyphen and be unique on a page.
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 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 should typically be used to set up initial state, default values, event listeners, and a shadow root.
class MyCustomElement extends HTMLElement {
constructor() {
super();
/* custom element created */
}
connectedCallback() {
/* element is added to document */
}
disconnectedCallback() {
/* element is removed from the document */
}
static get observedAttributes() {
return [/* array of attribute names to monitor for changes */];
}
attributeChangedCallback(name, oldValue, newValue) {
/* one of attributes listed above is modified */
}
adoptedCallback() {
/* element is moved to a new document */
}
}
customElements.define('my-custom-element', MyCustomElement);
Define the functionality you want using the lifecycle callbacks, such as connectedCallback() or disconnectedCallback().
Then, use this class to define the element using customElements.define("my-custom-element", MyCustomElement);.
Note
Creating customized built-in elements using the extends option isn't supported on the Salesforce Platform or in WebKit browsers such as Safari. To learn more about what you can do in a class that extends HTMLElement, see HTML Spec: Custom Elements.
Here's an example. Basic HTML markup is added to the custom element using the Element.attachShadow() method.
// main.js
import { createElement } from "lwc";
import App from "x/app";
const elm = createElement("x-app", { is: App });
document.body.appendChild(elm);
customElements.define('my-custom-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' })
.innerHTML = '<div>I am a third-party web component!</div>';
}
});
In closed mode, the shadowRoot property returns null, which means you can’t use the shadowRoot to access and manipulate the shadow root of the element. When creating custom elements in closed mode, save the reference to the shadow root with a different variable, such as _shadow or __shadowRoot.
this._shadow = this.attachShadow({ mode: 'closed' });
this._shadow.innerHTML = `<style></style><div></div>`;
Example
The lwc-recipes-oss repo has an externalComponentNpm component that imports and uses a third-party web component.
# Use the Custom Element or Third-Party Web Component in LWC
To render your custom element in LWC, add it to your template using the lwc:external directive.
<!-- app.html -->
<template>
<my-custom-element lwc:external></my-custom-element>
</template>
The component renders in the DOM like this.
<x-app>
#shadow-root (open)
<my-custom-element>
#shadow-root (open)
<div>I am a third-party web component!</div>
</my-custom-element>
</x-app>
# Pass Data to a Third-Party Web Component
Pass data to a third-party web component using an attribute, property, or the lwc:spread directive.
When passing data to a third-party web component, LWC sets the data as attributes by default, and sets properties only if they exist.
Note
A runtime check is performed on the element to see if a property is defined. If you set the data before the element is upgraded, the data is set as an attribute and is only available on upgrade via attributeChangedCallback. After the element has been upgraded and the property exists, subsequent renders set the property instead.
Consider the following third-party web component with properties.
<!-- app.html -->
<template>
<ce-with-property lwc:external
prop={data}
attr={data}
lwc:spread={myProps}>
</ce-with-property>
</template>
// app.js
import { LightningElement, api } from 'lwc';
export default class extends LightningElement {
@api data;
myProps = {
num: 54,
greeting: "Hello custom element"
};
}
Setting your data via a property preserves the data type. However, if you set data via an attribute, the value is returned as a string. For example, an object returns [object Object] via an attribute.
elm.data = {};
const ce = elm.shadowRoot.querySelector('ce-with-property');
ce.getAttribute('attr'); //[object Object]
const obj = {};
elm.data = obj;
ce.prop; // obj
To render the properties using lwc:spread on your template, use the ${attribute} syntax.
// main.js
customElements.define('ce-with-property', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' })
.innerHTML = `${this.greeting}` + ` ${this.num}`;
// renders "Hello World 54"
}
});
# Pass Markup to a Slot in Third-Party Web Component
Passing markup to a slot in a third-party web component behaves similarly to a slot in an LWC component.
Note
Slotting implementation for synthetic shadow is not supported in third-party web components.
Consider a third-party web component with some markup.
// main.js
customElements.define('ce-with-children', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<h1>My title</h1>
<div>
<p>Some content here</p>
</div>
<slot></slot>
`;
}
});
The following slotted content appears in the <slot> element.
<template>
<ce-with-children lwc:external>
<div class="slotted">slot content</div>
</ce-with-children>
</template>
The component renders in the DOM like this.
<ce-with-children>
#shadow-root (open)
<h1>My title</h1>
<div><p>Some content here</p></div>
<slot>
<div class="slotted">slot content</div>
</slot>
</ce-with-children>
# Work with Events in Third-Party Web Components
Events in third-party web components behave similarly with events in LWC. The event bindings support only lowercase events. To use events with non-lowercase names, add an event listener using the addEventListener() API.
Based on the HTML specifications, you typically use the constructor to set up event listeners.
customElements.define('ce-with-events', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `CLICK ME!`;
this.addEventListener('click', this.handleClick);
}
handleClick() {
this.dispatchEvent(new CustomEvent('lowercaseevent'));
this.dispatchEvent(new CustomEvent('camelEvent'));
}
});
# Example: Increment a Counter Using an Event Listener
This example creates a button that increments the counter on its label when pressed.
# Observe Attributes and Handle Change
After a third-party web component is rendered, attribute changes are ignored. To observe attributes and ensure the third-party web component renders your changes, use observedAttributes() static getter and attributeChangedCallback() method. You must observe the attributes to get the attributeChangedCallback() callback to fire when an attribute changes.
See the MDN lifecycle callback examples.
# 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.
myComponent
├──myComponent.html
├──myComponent.js
├──myComponent.js-meta.xml
└──myCustomElement.js
In the myCustomElement.js file, define and register the custom element.
// myCustomElement.js
class MyCustomElement extends HTMLElement {
constructor() {
super();
this._shadow = this.attachShadow({ mode: 'closed' });
this._shadow.innerHTML = `<div>Hello Custom Element</div>`;
}
}
customElements.define("my-custom-element", MyCustomElement);
To import the JavaScript file from your Lightning web component, use the import syntax.
// myComponent.js
import { LightningElement } from "lwc";
import "./myCustomElement.js";
export default class MyComponent extends LightningElement {}
In the HTML template file, create an instance of your custom element using lwc:external.
<!-- myComponent.html -->
<template>
<my-custom-element lwc:external></my-custom-element>
</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 x-my-custom-square component bundle. It also exports the random function, so that the x-my-custom-square component can import and use it.
The myCustomSquare.js file contains the click handlers for the buttons that adds, update, and removes the custom square from the DOM. It 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.
Important
We don’t recommend using JavaScript to manipulate the DOM. It's better to use template directives to write declarative code. For example, use lwc:if to conditionally display an element or component instead of using appendChild and removeChild. Using the conditional directive also triggers the disconnectedCallback and connectedCallback lifecycle callbacks for the custom element.
# Third-Party Web Component Considerations
An external component that isn't registered renders as an instance of the native HTMLUnknownElement interface, which extends HTMLElement without adding any properties or methods. The browser then treats the external component as a native component no different from a span or a div.
For registered components, the engine renders the associated third-party web component and defers the upgrading to the browser.
For more information, see HTML Spec: Upgrading elements after their creation.
Additionally, consider these upgrade behaviors on third-party web components.
- If a third-party web component is not upgraded, LWC sets its attributes on mount and on update.
- If there's a delayed upgrade, the attribute is set instead of the property.
- After the upgrade, the property is set instead of the attribute, if the property exists.
Let's say a delayed upgrade is performed on the third-party web component.
<!-- app.html -->
<template>
<ce-with-delayed-upgrade lwc:external foo="bar">
</ce-with-delayed-upgrade>
</template>
After the upgrade, the new property value is set on the element.
// after upgrade
ce.getAttribute('foo') // "bar"
ce.foo // null or undefined