TL;DR: new CustomElementRegistry() gives a shadow root its own private set of custom element definitions, so two libraries can both own a <ui-button> on the same page. Firefox 159 shipped it on October 27, 2026, which makes scoped custom element registries Baseline Newly available.
The name nobody can share
Custom elements have always lived in one global phone book: window.customElements. Every tag name is registered once, for the whole page, forever.
So your app defines <ui-button>. Then you install a date picker that also ships a <ui-button> inside it. One of them loses:
customElements.define("ui-button", AppButton); customElements.define("ui-button", PickerButton); // NotSupportedError: Failed to execute 'define' on 'CustomElementRegistry': // the name "ui-button" has already been used with this registry That is the exact message Chrome 156 threw when I ran it. The usual fixes are prefixes (<acme-ui-button>), version suffixes (<ui-button-v2>), or a build step that renames tags. They work. They also turn every tag name into a small legal contract.
What it is
A scoped registry is a CustomElementRegistry you create yourself with new. Definitions added to it with define() are not global. They only apply to nodes associated with that registry.
You associate a registry with a subtree in three ways:
- pass it to
attachShadow()ascustomElementRegistry, - call
registry.initialize(shadowRoot)on a shadow root that has no registry yet, - pass it to
document.createElement()as thecustomElementRegistryoption.
Everything outside that subtree keeps using the global customElements, exactly as before.
The minimal one: two buttons, one name
const vendorA = new CustomElementRegistry(); const vendorB = new CustomElementRegistry(); vendorA.define("ui-button", class extends HTMLElement { connectedCallback() { this.textContent = "A"; } }); vendorB.define("ui-button", class extends HTMLElement { connectedCallback() { this.textContent = "B"; } }); const hostA = document.createElement("div"); const hostB = document.createElement("div"); document.body.append(hostA, hostB); const rootA = hostA.attachShadow({ mode: "open", customElementRegistry: vendorA }); const rootB = hostB.attachShadow({ mode: "open", customElementRegistry: vendorB }); rootA.innerHTML = "<ui-button></ui-button>"; rootB.innerHTML = "<ui-button></ui-button>"; In Chrome 156 the first button rendered A and the second rendered B. Same tag, two definitions, no error. rootA.customElementRegistry === vendorA was true, and the global registry did not learn about either class.
The realistic one: a component that keeps its internals to itself
This is the case that actually hurts: a library component that uses helper elements inside its own shadow DOM, on a page that already has its own elements with the same names.
// The app's own button, registered globally as always. customElements.define("ui-button", class extends HTMLElement { connectedCallback() { this.dataset.from = "app"; } }); // Inside the date picker library. const pickerRegistry = new CustomElementRegistry(); pickerRegistry.define("ui-button", class extends HTMLElement { connectedCallback() { this.dataset.from = "picker"; this.setAttribute("role", "button"); } }); customElements.define("date-picker", class extends HTMLElement { constructor() { super(); const root = this.attachShadow({ mode: "open", customElementRegistry: pickerRegistry, }); root.innerHTML = ` <ui-button>Prev</ui-button> <span>Nov 2026</span> <ui-button>Next</ui-button>`; } }); <ui-button>Save</ui-button> <date-picker></date-picker> I ran this on a clean page in Chrome 156. The page's Save button got data-from="app". Both buttons inside the picker got data-from="picker" and role="button". The library never touched the global ui-button, and the app never had to know the picker had one.
Note that <date-picker> itself is still global. Scoping hides a component's internals; the outer tag you put in your HTML still needs a unique name.
Can I use it?
| Where | Status |
|---|---|
| Baseline | Newly available since October 27, 2026 |
| Safari (macOS and iOS) | 26 |
| Chrome and Edge | 146 |
| Firefox | 159 |
| Tested | Chrome 156, where every result in this post was produced |
| Spec | HTML Standard (CustomElementRegistry) |
Safari 26.0 shipped it first, and Safari 26.4 added upgrading elements in initialize() plus fixes around null registries. If you support Safari 26.0 to 26.3, test the initialize() path there before relying on it.
For older browsers, "initialize" in CustomElementRegistry.prototype is a cheap feature check.
Gotchas
document.createElement()still uses the global registry by default. Appending an element does not move it to the new registry; it keeps the one it was created with. I created aui-buttonwith plaindocument.createElement("ui-button")and appended it into the picker's shadow root. It came out withdata-from="app". Pass{ customElementRegistry: root.customElementRegistry }as the second argument, or create the markup through the shadow root'sinnerHTML, and it came outpicker.initialize()is first come, first served. I calledone.initialize(root)and thentwo.initialize(root)on the same root. Chrome did not throw, and the root keptone. Decide which code owns a shadow root's registry and set it once.Declarative shadow DOM needs an opt-in and a follow-up. Server-rendered shadow roots get the global registry unless the template says otherwise:
<my-host> <template shadowrootmode="open" shadowrootcustomelementregistry> <ui-button></ui-button> </template> </my-host> With the attribute, shadowRoot.customElementRegistry was null and the button did not match :defined. After registry.initialize(host.shadowRoot), it upgraded and matched :defined. Without the attribute, the root used the global customElements.
Wrap-up
Prefixes were a social workaround for a technical limit, and the limit is gone. Pick one component that ships its own helper elements, give its shadow root a registry, and drop a prefix.
Sources
- CustomElementRegistry() constructor (MDN)
- Using custom elements: Scoped custom element registries (MDN)
- Firefox 159 release notes for developers (MDN)
- Chrome 146 release notes (Chrome for Developers)
- WebKit Features for Safari 26.4 (WebKit blog)
- Newly available features (web-features explorer)
This article was written with the help of AI and checked against the sources above.