Published: March 9, 2026
Custom elements let web app developers build, share, and reuse UI components with their unique behaviors, making development easier. But when your app brings together different sets of custom elements, things can get messy, and name collisions can occur. Scoped custom element registries solve this issue!
The Microsoft Edge team has worked on this feature, and we're excited to announce that scoped custom element registries are now available by default from Edge and Chrome 146, as well as other Chromium-based browsers. You can now encapsulate custom elements, solving a long-standing pain point for developers of components and micro-frontend libraries.
Scoped custom element registries unlock important patterns for web developers. Now, you can use multiple custom element libraries, developed independently by multiple teams, or multiple versions of the same library side-by-side.

What's a scoped custom element registry?
Today, every custom element definition on a webpage lives in a single, shared
registry at window.customElements. This means that if two different libraries
both try to define a custom element with the same tag name, like <my-button>,
an error will be thrown and the page will break. This is a significant problem
in the real world. Large applications that compose their user interfaces from
multiple teams, design systems, or micro-frontends can easily run into naming
collisions. Scoped custom element registries solve this by letting you create
independent registries. Each registry maintains its own set of custom element
definitions, completely isolated from the global registry and from each other.
Create a new registry
Instead of defining every custom element in the global window.customElements
registry:
- Create a new registry by calling
new CustomElementRegistry(). - Give your new registry a specific scope (see Scoping your registry).
- Define the elements that go into the new registry.
Then, when a custom element is used, the browser looks up the element's definition from the associated registry, which isn't necessarily the global one. This means that different parts of your page can use entirely different sets of custom element definitions.
Scope your registry
A custom element registry can be scoped to either a document, a shadow root, or an individual element.
Scope to a shadow root
To scope a new registry to a shadow root, use the customElementRegistry option
when calling the
attachShadow()
method. All the custom elements that are inside that shadow root will now use
the definition that's in the corresponding scoped registry:
// Create your custom registry.
const registry = new CustomElementRegistry();
// Define a custom element in the new registry.
registry.define('my-card', class extends HTMLElement {
connectedCallback() {
this.textContent = 'Hello from scoped registry!';
}
});
// Create shadow root, providing it with your new custom element registry.
const host = document.querySelector('#host');
const shadow = host.attachShadow({
mode: 'open',
customElementRegistry: registry,
});
shadow.innerHTML = '<my-card></my-card>';
Declarative shadow DOM
Use the
shadowrootcustomelementregistry
attribute on a <template> element to tell the browser that the resulting
shadow root is using a scoped registry rather than the global one:
<my-host>
<template shadowrootmode="open" shadowrootcustomelementregistry>
<my-widget></my-widget>
</template>
</my-host>
const registry = new CustomElementRegistry();
registry.define('my-widget', class extends HTMLElement {
connectedCallback() { this.textContent = 'Scoped!'; }
});
const shadow = document.querySelector('my-host').shadowRoot;
registry.initialize(shadow);
As shown in the example, the attribute reserves space for the custom element registry, and the user needs to define and initialize the registry to the shadow root Scoping to a disconnected document
You can also scope a registry to a document, such as one created by
document.implementation.createHTMLDocument(). This lets you clone
<template> elements, and do off-screen document manipulation, each with
their own isolated sets of component definitions. To do this, use the
CustomElementRegistry.initialize()
method:
// Create a new registry.
const registry = new CustomElementRegistry();
registry.define('app-widget', AppWidget);
// Create a document, and use registry.initialize() to scope the registry to the document.
const doc = document.implementation.createHTMLDocument('');
registry.initialize(doc);
doc.body.innerHTML = '<app-widget></app-widget>';
Scope to an individual element
You can also associate a registry directly with an element and its subtree by
passing the customElementRegistry option to document.createElement(). The
element and its descendants will resolve against that registry, no matter which
part of the DOM they are later inserted into:
// Create a registry and define a custom element in it.
const registry = new CustomElementRegistry();
registry.define('fancy-label', FancyLabel);
// Create a new DOM element and scope the new registry to it.
const el = document.createElement('fancy-input', {
customElementRegistry: registry,
});
// Use a custom element in the new DOM element.
el.innerHTML = '<fancy-label>Name</fancy-label>';
Learn more
To learn more, check out the Edge demo and its source code, and read MDN's CustomElementRegistry reference article
Scoped custom element registries are now enabled by default in both Microsoft Edge and Chrome, as well as in other Chromium-based browsers, thanks to our collaboration through the Chromium project.
You don't need to enable any setting or sign-up to an origin trial. You can just start using the feature today in Chromium-based browsers.
If you need this feature to be implemented in other browsers, give the Scoped custom element registries issue a thumbs-up reaction, and leave a comment with your use cases and workarounds.
If you find a bug with the implementation of the feature in a Chromium-based browser, open an issue for us to investigate at crbug.com/new.
We hope that scoped custom element registries make using web components easier.