Querying through shadow roots
Let's say I have a web component with an open shadow root, like this one from Shoelace.
<sl-button>Click me</sl-button>
Inside the shadow root is a <button>
that I want to target with JavaScript.* Alas, Element.querySelector()
doesn't offer a shortcut for traversing shadow roots so we have to do this.
const button = document.querySelector('sl-button').shadowRoot.querySelector('button');
That's pretty verbose! It's nice that we can chain the selectors, but it would be even nicer if we could poke through shadow roots right in the selector.
// This doesn't work, but we can dream
const button = document.querySelector('sl-button >>> button');
Well, here's a TypeScript function that gets us pretty close to that.
function shadowQuery(selector: string, rootNode: Document | Element = document): Element | null {
const selectors = String(selector).split('>>>');
let currentNode = rootNode;
selectors.find((selector, index) => {
if (index === 0) {
currentNode = rootNode.querySelector(selectors[index]) as Element;
} else if (currentNode instanceof Element) {
currentNode = currentNode?.shadowRoot?.querySelector(selectors[index]) as Element;
}
return currentNode === null;
});
if (currentNode === rootNode) {
return null;
}
return currentNode as Element | null;
}
This let's you use >>>
in your selector instead of splitting it into multiple queries, resulting in a much simpler syntax.
const button = shadowQuery('sl-button >>> button');
Querying starts on document
by default, but you can pass a node as the second argument to change that.
const container = document.querySelector('.your-root-node');
const button = shadowQuery('sl-button >>> button', container);
Finally, you can even traverse multiple shadow roots in one query.
shadowQuery('my-element >>> my-second-element >>> my-third-element');
*It's worth noting that you probably shouldn't be targeting shadow roots — they're encapsulated for a reason! Nevertheless, this can be very useful in exceptional situations.