Chrome feature can leave Web Components empty if one script fails

Code from Chrome for Developers: Scoped custom element registries: const registry = new C…
Code: Chrome for Developers, "Scoped custom element registries"

Summary

Web Components that opt into Chrome's scoped registries depend on one more script. Elements inside the marked shadow root stay undefined until that script sets up the registry, and site-wide definitions do not fill in if it fails.

Put indexable text directly in the HTML inside the template, not in component code. On pages that use the new attribute, compare the raw HTML response with the rendered page.

Chrome and Edge 146 turn on scoped custom element registries by default, according to Chrome’s announcement post from 2026-03-09. The feature fixes a real conflict. Today every custom element definition on a page lives in one shared registry at window.customElements. If two libraries both define <my-button>, the browser throws an error and the page breaks. A scoped registry gives a part of the page its own set of definitions.

For server-rendered components, one detail in the post matters more than the rest. Declarative shadow DOM lets a component’s shadow root (its private internal markup) be written straight into the HTML. web.dev’s guide explains that the parser applies such a template immediately, and that no JavaScript is needed to build the tree. That is why it suits search, as covered in an earlier piece on how Web Components can render from HTML before any JavaScript runs.

Chrome’s post adds a shadowrootcustomelementregistry attribute for that template. The attribute tells the browser the shadow root uses a scoped registry instead of the global one. It does not supply the registry. In the post’s words, the attribute “reserves space” for it, and a script still has to create the registry, define the elements, and call registry.initialize() on the shadow root.

Chrome’s own example shows the gap. The HTML contains an empty <my-widget> inside the template. The text “Scoped.” is written by the component’s connectedCallback, the function a custom element runs when it joins the page. That function runs only after a script defines my-widget in the new registry and initializes it on the shadow root. Until then, the served HTML holds an empty tag.

Text written in component code always needed JavaScript. What the attribute adds is a second dependency. Because the shadow root is marked as using a scoped registry, the browser does not look up its elements in the global one. The likelier consequence is that a component the site already defines globally stays empty inside that shadow root if the initialize script fails. The page can still look fine to a developer who tested with the script working.

The post does not discuss search. Google renders JavaScript, so Googlebot will likely see the text once the script succeeds. A crawler or AI fetcher that reads only the HTML response sees the empty tag either way. The safer approach is to keep anything that should be indexed in the template markup, out of component code.

What to do

  1. Write indexable text into the template markup rather than setting it in connectedCallback. The component can still upgrade the element later; the words are already in the HTML.
<my-host>
  <template shadowrootmode="open" shadowrootcustomelementregistry>
    <my-widget>Scoped!</my-widget>
  </template>
</my-host>
  1. Search your templates and served HTML for shadowrootcustomelementregistry to find which pages use it. Design systems and micro-frontend setups, the post’s main use cases, are the likeliest places.
  2. On those pages, compare the raw HTML response with the rendered HTML in Search Console’s URL Inspection. Text that appears only in the rendered version depends on the registry script.
  3. Load the script that calls registry.initialize() with the page, not behind a user interaction.