Skip to content

Option to allow querySelector to query shadow roots #1422

Open

Description

@LeaVerou

What problem are you trying to solve?

This is a counterpart of #1287. #1287 is about monitoring future changes, whereas this is about observing the current state. Both are necessary for these use cases (polyfilling future HTML syntax), since as script cannot know at what point it is dropped in a page, and what already exists.

What solutions exist today?

Current workarounds are even worse than those described in #1287, since monkey-patching attachShadow is no help here, so the only solution is to walk the DOM and check its shadowRoot. And without something like w3c/csswg-drafts#11007 you need to literally walk every single element.

Here is a complete userland implementation:

function querySelectorAll (selector, root = document, options = {}) {
	let shadowRoots = options.shadowRoots?.roots ?? (Array.isArray(options.shadowRoots) ? options.shadowRoots : []);
	shadowRoots = [...shadowRoots];
	let openRoots = options.shadowRoots?.open || options.shadowRoots === true;
	if (openRoots) {
		shadowRoots.push(...getShadowRootsUnder(root));
	}

	let ret = [...root.querySelectorAll(selector)];
	for (const shadowRoot of shadowRoots) {
		ret.push(...shadowRoot.querySelectorAll(selector));
	}
	return ret;
}

function getShadowRootsUnder (root = document, recursive = true) {
	let ret = root.shadowRoot ? [root.shadowRoot] : [];
	ret.push(...[...root.querySelectorAll("*")]
		.flatMap(element => {
			if (!element.shadowRoot) return [];

			let ret = [element.shadowRoot];
			if (recursive) {
				ret.push(...getShadowRootsUnder(element.shadowRoot));
			}
			return ret;
		}));
	return ret;
}

As you can see, it is nontrivial, not to mention very inefficient.

How would you solve it?

Ideally, we want to be able to:

  1. Query all open shadow roots, recursively
  2. Pass specific shadow roots by reference (which allows querying certain closed roots)

We could add an options bag after the first argument, with a shadowRoots option with type boolean | Iterable<ShadowRoot> | { open: boolean, roots: ShadowRoot[] }

If making it that easy to query open shadow roots is not desirable, an MVP could be just an Iterable<ShadowRoot> which at least makes it easier and paves the way for later expansion. Though I don't see the point: if authors need to query all open roots, they just have to implement it in userland with the same or worse performance. Even if currently UAs don't store a data structure for quickly finding all open roots, once there is API surface that allows authors to do this, they could lazily instantiate such a structure and use it on all subsequent calls, updating it only when a new shadow root is attached, whereas authors need to redo the entire walking every time.

We should probably decide on what the API shape should look like for all DOM methods we want to enable such functionality on (e.g. #1189 ) rather than re-inventing the wheel on every single one.

Anything else?

The following issues are also relevant:

Activity

  1. jonathanKingston commented on Jan 12, 2026

    @jonathanKingston
    Contributor

    This seems really well thought out and certainly solves an annoying problem.


    The one caveat here that the snippet doesn't solve is selectors that cross the boundaries; I think that's very intentional. My guess is the CSS selectors would assist with this optionality in future:

    document.querySelectorAll('div button, div :host button', {
      shadowRoots: true
    })

    Or:

    document.querySelectorAll('div button, div :host button', {
      shadowRoots: {
        open: true,
        selectAcrossRoots: true
      }
    })

    In short: I think this is a non issue to solve as part of this proposal.

  2. jonathanKingston commented on Jan 27, 2026

    @jonathanKingston
    Contributor

    @jakearchibald you were discussing this lately with $$$ in the Firefox console, I'd be interested in prototyping it if you think there's support in landing it.

  3. jakearchibald commented on Jan 27, 2026

    @jakearchibald
    Collaborator

    I don't know if there's support for landing it, but in terms of API design it might be worth looking at things like https://developer.mozilla.org/en-US/docs/Web/API/Selection/getComposedRanges which take an array of shadow roots. There isn't a way to include all open shadow roots there - I wonder if that was a deliberate choice and if the reasons are documented.

  4. jonathanKingston commented on Jan 30, 2026

    @jonathanKingston
    Contributor

    Thanks for the reference, I wasn't aware of that! I think the use-case is pretty different in regards to passing shadow roots, it seems that it returns a range of any of the shadows entirely when contained within the shadow root.

    https://www.mail-archive.com/blink-dev@chromium.org/msg13171.html discusses that:

    If the selection crosses a shadow root boundary that isn’t provided in the shadowRots list, then the StaticRange's endpoints will be “rescoped” to be outside that tree. This makes sure we do not expose unknown shadow trees.

    So I think the read is that because this provides ranges, it needs to propagate up to known open ones. I don't think this is a performance concern.

  5. jakearchibald commented on Jan 30, 2026

    @jakearchibald
    Collaborator

    I think the use-case is pretty different in regards to passing shadow roots

    The utility of the API is different, but the pattern of "these are the shadow roots you're allowed to see inside" is the same.

    This pattern is also used in el.getHTML(), and resolved to add to getElementsFromPoint.

    Basically, if you're going to propose a different way to give an API visibility into shadow roots, you'll need to justify why it's different to established patterns.

  6. annevk commented on Jan 30, 2026

    @annevk
    Member

    Essentially the constrain we try to place upon such APIs is that they work equally well for closed and open shadow roots, without regressing on encapsulation.

  7. jonathanKingston commented on Jan 30, 2026

    @jonathanKingston
    Contributor

    Well I don't think in this case this can work well for closed shadow routes (but that's the status quo of querySelector anyway). So the next best thing is allow the design to be specific that it is open shadow routes only, right?

    What's the best way to try and drum up implementer interest here? (I did read the docs a bit but couldn't find anything quickly, sorry for the noise)

  8. annevk commented on Jan 30, 2026

    @annevk
    Member

    I think the same shape as the other APIs would make this work for closed shadow trees? I doubt you’ll get sufficient implementer interest if you don’t solve for that.

  9. jakearchibald commented on Jan 30, 2026

    @jakearchibald
    Collaborator

    @jonathanKingston

    Well I don't think in this case this can work well for closed shadow routes

    Why not? Closed shadow roots can contain elements too.

    Imagine we had an API to get all open shadow roots, then you could have:

    const videoElements = document.querySelectorAll('video', {
      shadowRoots: [...document.getOpenShadowRoots(), ...someSpecificClosedRoots],
    });
  10. jonathanKingston commented on Jan 30, 2026

    @jonathanKingston
    Contributor

    Ah, sorry, yeah, I understand. I thought you were suggesting it should query into shadow dom if I didn't have a reference.

    That makes sense to me, and I think what Lea has suggested, even though the user land script would not work in that fashion, there's nothing to suggest that the real API wouldn't be able to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interesttopic: shadowRelates to shadow trees (as defined in DOM)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions