Preventing Custom Element Name Collisions with Scoped Custom Element Registries
When you build with Web Components, name collisions happen if two libraries define custom elements with the same name. Scoped Custom Element Registries let you register different implementations under the same name in separate scopes on one page.
Web Components is an umbrella term for the technologies used to build reusable, custom HTML elements. It consists mainly of Custom Elements, which define your own element names and behavior; Shadow DOM, which encapsulates DOM structure and styles; and the
<template> and <slot> elements, which declare reusable markup. These are standard APIs provided by the browser, and their defining trait is that you can use them without depending on a framework such as React or Vue.js.
Web Components are often used to ship UI components — buttons, dialogs, input forms — as a library, and projects such as Material Web Components and
Shoelace are publicly available. Beyond that, Web Components are also used to distribute UI components that follow a product's design system for internal use, or to share a common set of UI components across several projects in a micro-frontend architecture.
Users of these libraries load a JavaScript module and then write custom elements such as <my-button> just like ordinary HTML. Because a library built on Web Components can be used without depending on a framework, it is especially convenient when several internal projects need to share the same UI components.
One of the barriers to using Web Components libraries, however, is name collisions. When multiple libraries define a custom element with the same name, only one of the definitions can be used. For example, if one library defines <my-card> and another library tries to define the same <my-card>, whichever registers second fails with an exception. Today we rely on prefix naming conventions (md-, sl-, and so on) to avoid collisions — an approach that is far from foolproof.
Scoped Custom Element Registries solve this problem, allowing custom elements to be encapsulated. You can create multiple CustomElementRegistry instances on the same page and register a custom element with the same name in each of them.
// Create a registry
const registry = new CustomElementRegistry();
// Register a custom element in the registry
class MyCard extends HTMLElement {}
registry.define("my-card", MyCard);
// Create a Shadow Root and associate the registry with it
const host = document.querySelector("#host");
const shadowRoot = host.attachShadow({
mode: "open",
customElementRegistry: registry,
});
// Use <my-card> inside the Shadow Root
shadowRoot.innerHTML = `<my-card></my-card>`;This article gives an overview of Scoped Custom Element Registries.
The conventional way to register a custom element
Let's start by reviewing how custom elements are created the conventional way. The behavior of a custom element is defined as a class that extends HTMLElement.
class MyCard extends HTMLElement {
connectedCallback() {
this.textContent = "My Card";
}
}connectedCallback() is a lifecycle callback invoked when the custom element is connected to the document. In this example, it sets the element's text to My Card.
Creating the class alone does not tell the browser which HTML element the behavior applies to. You register the custom element with a registry by passing the element name and the class to the customElements.define() method. A registry is the mechanism that manages the mapping between a custom element's name and the JavaScript class implementing that element's behavior. The registry is exposed globally as window.customElements.
customElements.define("my-card", MyCard);A custom element name must contain a hyphen so that it is distinguishable from a standard HTML element. Once registered, it can be used just like any other HTML element.
<my-card></my-card>When customElements.define() is called, the MyCard definition is applied to any <my-card> already present in the document. Applying a custom element definition to existing elements as a result of registering it with a registry is called an upgrade.
window.customElements is a global CustomElementRegistry shared across the entire document. If you try to register a different class under the same name, as shown below, a NotSupportedError is thrown.
class AnotherCard extends HTMLElement {}
customElements.define("my-card", AnotherCard);
// Uncaught NotSupportedError
// Failed to execute 'define' on 'CustomElementRegistry': the name "my-card" has already been used with this registryThat is why you have to pay attention to custom element names when combining multiple libraries on the same page.
If the only components involved are the ones your application owns, a naming convention such as a prefix may be enough to avoid collisions. But when different versions of a library are pulled in as transitive dependencies, or when you combine plugins, browser extensions, and third-party widgets, coordinating names across the whole page becomes difficult.
Creating a Scoped Custom Element Registry
Unlike the global registry, a Scoped Custom Element Registry is created from the CustomElementRegistry constructor. The constructor takes no arguments.
class DemoCard extends HTMLElement {}
const registry = new CustomElementRegistry();
registry.define("demo-card", DemoCard);Registering a custom element with the registry's define() method is not enough to use the element in HTML. A Scoped Custom Element Registry requires you to explicitly specify which DOM tree it applies to. The custom element only takes effect within the scope of that DOM tree.
To associate the registry with a Shadow Root, pass it to the customElementRegistry option of the attachShadow() method.
const host = document.querySelector("#host");
const shadowRoot = host.attachShadow({
mode: "open",
customElementRegistry: registry,
});
shadowRoot.innerHTML = `<demo-card></demo-card>`;A Shadow Root is the root node of a subtree that is rendered separately from the document's DOM tree. An element that has a Shadow Root is called a Shadow Host, and the Shadow Root together with its descendants is called the Shadow DOM. A Shadow Root lets you isolate a component's internal HTML structure and styles from the outside.
A registry can be associated not only with a Shadow Root but also with an ordinary Element. Using the registry created above, let's pass customElementRegistry as the second argument to document.createElement(). This scopes the registry to the created element and its descendants without using Shadow DOM.
const card = document.createElement("demo-card", {
customElementRegistry: registry,
});
document.body.append(card);Within a tree associated with a Scoped Custom Element Registry, definitions registered in the global customElements become invisible. Looking up a custom element definition consults only the single registry associated with the node, and there is no fallback to the global registry. As a result, any custom element used in that scope has to be registered with the registry again, even if it is already registered globally.
Registering different implementations under the same element name
Now let's create two Scoped Custom Element Registries and register the same name, demo-card, in each of them. Using two registries lets you assign different implementations to custom elements with the same name on a single page.
First, prepare two host elements for creating Shadow Roots.
<div id="host-a"></div>
<div id="host-b"></div>Next, define two custom elements, BlueCard and OrangeCard, that render different content.
class BlueCard extends HTMLElement {
connectedCallback() {
this.textContent = "BlueCard";
}
}
class OrangeCard extends HTMLElement {
connectedCallback() {
this.textContent = "OrangeCard";
}
}Then create the two registries. Both use the same name, demo-card, but because the registries differ, both registrations succeed without a problem.
const registryA = new CustomElementRegistry();
const registryB = new CustomElementRegistry();
registryA.define("demo-card", BlueCard);
registryB.define("demo-card", OrangeCard);Create a Shadow Root on each of the two host elements and associate a different registry with each one. Then insert the same <demo-card> into both Shadow Roots.
const shadowRootA = document.querySelector("#host-a").attachShadow({
mode: "open",
customElementRegistry: registryA,
});
const shadowRootB = document.querySelector("#host-b").attachShadow({
mode: "open",
customElementRegistry: registryB,
});
shadowRootA.innerHTML = `
<style>
demo-card {
display: block;
padding: 16px;
color: blue;
border: 2px solid blue;
}
</style>
<demo-card></demo-card>
`;
shadowRootB.innerHTML = `
<style>
demo-card {
display: block;
padding: 16px;
color: orange;
border: 2px solid orange;
}
</style>
<demo-card></demo-card>
`;When this code runs, the <demo-card> in registryA is upgraded to BlueCard and the one in registryB to OrangeCard. The CSS selector is demo-card in both cases, but because the custom element definitions and the styles are scoped to their own Shadow Root, the two render with different content and different colors. The demo-card styles written in one Shadow Root never apply to the <demo-card> in the other.
Associating a registry after the Shadow Root has been created
When you cannot pass the registry at the time attachShadow() is called, you can use the CustomElementRegistry.prototype.initialize() method. It associates the registry with an Element or ShadowRoot that does not have a registry yet.
const registry = new CustomElementRegistry();
registry.define("demo-card", DemoCard);
registry.initialize(shadowRoot);One of its main use cases is declarative Shadow DOM. Specifying the shadowrootcustomelementregistry attribute on a <template> element creates a Shadow Root with no registry set. You can associate a registry with this Shadow Root — created earlier by the HTML parser — once your JavaScript runs.
<div id="host">
<template shadowrootmode="open" shadowrootcustomelementregistry>
<style>
demo-card {
display: block;
padding: 16px;
color: purple;
border: 2px solid purple;
}
</style>
<demo-card></demo-card>
</template>
</div>registry.initialize(document.querySelector("#host").shadowRoot);initialize() initializes the target and any of its descendants that do not have a registry set, and it also attempts to upgrade custom elements using the definitions already registered.
Summary
- When building an application with Web Components, a name collision occurs if different component libraries define custom elements with the same name
- Scoped Custom Element Registries let you assign separate implementations to custom elements with the same name in different scopes on the same page
- By associating a registry with an
Elementor aShadowRoot, you can choose which custom element definitions to use per DOM-creation context




