A Beautiful Site

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 function that gets us pretty close to that.

function shadowQuery(selector, rootNode = document) {
  const selectors = String(selector).split('>>>');
  let currentNode = rootNode;

  selectors.find((selector, index) => {
    if (index === 0) {
      currentNode = rootNode.querySelector(selectors[index]);
    } else {
      currentNode = currentNode?.shadowRoot?.querySelector(selectors[index]);
    }

    return currentNode === null;
  });

  return currentNode;
}

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.