Stop Prefixing Every Custom Element Name. Scoped Registries Are Baseline
TL;DR: new CustomElementRegistry() gives a shadow root its own private set of custom element definitions, so two libraries can both own a 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 . Then you install a date picker that also ships a 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 (), version suffixes (), 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() as customElementRegistry, call registry.initialize(shadowRoot) on a shadow root that has no registry yet, pass it to document.createElement() as the customElementRegistry option. 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 = ""; rootB.innerHTML = ""; 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 = ` Prev Nov 2026 Next`; } }); Save 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 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 a ui-button with plain document.createElement("ui-button") and appended it into the picker's shadow root. It came out with data-from="app". Pass { customElementRegistry: root.customElementRegistry } as the second argument, or create the markup through the shadow root's innerHTML, and it came out picker. initialize() is first come, first served. I called one.initialize(root) and then two.initialize(root) on the same root. Chrome did not throw, and the root kept one. 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: 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.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to