Skip to content

Scoped CustomElementRegistry fails with "Illegal constructor" in isolated content scripts #1024

@kyle7zhang

Description

@kyle7zhang

Summary

Using the Scoped Custom Element Registries API inside an isolated-world content script fails with:

Uncaught TypeError: Failed to construct 'HTMLElement': Illegal constructor

The same code works correctly when run from a MAIN-world content script.

Background

Isolated content scripts cannot access the page's window.customElements (the global CustomElementRegistry). This is expected and intentional for isolation. The Scoped Custom Element Registries API seemed like a natural fit — it was designed to allow element definitions in a separate registry without polluting the host page's namespace.

However, the WICG proposal mentions browser extensions only as a motivating example for naming conflict isolation, implicitly assuming a MAIN-world context where the extension and page share the same JS realm. It does not address isolated worlds at all.

The failure in isolated content scripts is a distinct, deeper problem: cross-realm HTMLElement inheritance. When a content script class extends HTMLElement, that HTMLElement comes from the extension's isolated JS realm — a different object identity from the document's HTMLElement. The scoped registry's constructor validation rejects it. The proposal itself acknowledges a related constraint:

"it must limit constructors by default to only looking up registrations from the global registry. If the constructor is not defined in the global registry, it will throw."

Reproduction

The following two approaches both fail in an isolated content script:

Approach 1 — attach registry via attachShadow option:

const scopedRegistry = new CustomElementRegistry();
scopedRegistry.define("my-counter", Counter);

const container = document.createElement("div");
container.attachShadow({
  mode: "open",
  customElementRegistry: scopedRegistry,
});
container.shadowRoot.innerHTML = `<my-counter startvalue="1" step="3"></my-counter>`;
document.body.appendChild(container);

Approach 2 — initialize after attaching shadow:

const scopedRegistry = new CustomElementRegistry();
scopedRegistry.define("my-counter", Counter);

const container = document.createElement("div");
container.attachShadow({ mode: "open", customElementRegistry: null });
container.shadowRoot.innerHTML = `<my-counter startvalue="1" step="3"></my-counter>`;
scopedRegistry.initialize(container.shadowRoot);
document.body.appendChild(container);

Both throw:

Uncaught TypeError: Failed to construct 'HTMLElement': Illegal constructor

Impact

Web Components / Custom Elements are increasingly common in modern UIs. Many browser extension use cases — UI injection, overlays, agent-assist panels — benefit from Shadow DOM encapsulation. Without working Custom Elements in isolated content scripts, developers must either:

  • Run in MAIN world (losing isolation guarantees), or
  • Use an iframe (complex, limited host-page integration).

Discussion

We'd love to understand whether supporting Custom Elements (including scoped registries) in isolated-world content scripts is something WECG would be open to exploring. The cross-realm HTMLElement issue seems like it would need to be addressed either at the spec level or through browser-specific adaptation of the isolated world environment — and we're happy to help think through the design if that's useful.

References

Browser Tested

  • Chrome Version 149.0.7827.103 (Official Build) (arm64)
  • Manifest V3, isolated world content script

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions