npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

query-selector-shadow-dom-modern

v1.1.0

Published

Modern drop-in replacement for query-selector-shadow-dom — querySelector that pierces Shadow DOM roots without knowing the path

Readme

query-selector-shadow-dom-modern

Release

Modern drop-in replacement for query-selector-shadow-dom. Zero dependencies, full TypeScript support.

querySelector that can pierce Shadow DOM roots without knowing the path through nested shadow roots. Useful for automated testing of Web Components (Selenium, Puppeteer, Playwright, etc.).

Heads-up if you are migrating: one upstream bug is fixed — the child combinator > now follows the CSS spec instead of degrading into a descendant combinator. It is the only behavioural difference from query-selector-shadow-dom; see Fixed: the child combinator.

Install

npm install query-selector-shadow-dom-modern
# or
pnpm add query-selector-shadow-dom-modern
# or
yarn add query-selector-shadow-dom-modern

Usage

import { querySelectorDeep, querySelectorAllDeep, collectAllElementsDeep } from 'query-selector-shadow-dom-modern';

// Find first matching element (pierces shadow roots)
const btn = querySelectorDeep('.btn-in-shadow-dom');

// Find all matching elements
const items = querySelectorAllDeep('my-component .item');

// Collect all elements on the page, including shadow DOM
const all = collectAllElementsDeep();
const filtered = collectAllElementsDeep('a[href]');

UMD (browser)

<!-- Via CDN, minified (~2.5 KB gzipped) -->
<script src="https://cdn.jsdelivr.net/npm/query-selector-shadow-dom-modern/dist/umd/index.min.js"></script>
<!-- Or unpkg -->
<script src="https://unpkg.com/query-selector-shadow-dom-modern/dist/umd/index.min.js"></script>
<!-- Debuggable full build (with sourcemap): use dist/umd/index.js instead of index.min.js -->
<script>
  const btn = querySelectorShadowDom.querySelectorDeep('.btn-in-shadow-dom');
</script>

API

  • querySelectorDeep(selector, root?) — Returns the first matching element, piercing shadow roots
  • querySelectorAllDeep(selector, root?) — Returns an array of all matching elements, piercing shadow roots
  • collectAllElementsDeep(selector?, root?) — Collects all elements on the page, including those within shadow roots. Optionally filters by a CSS selector

Both root defaults to document. Pass a custom root to scope the search (e.g., an iframe's contentDocument). All three also accept the original library's third argument (a pre-collected element list).

Drop-in compatibility

Verified against [email protected] with a differential harness that loads both libraries into the same page and compares results by element identity:

pnpm verify:diff                           # deterministic matrix (14 fixtures x 78 selectors)
pnpm verify:diff -- --fuzz=4000 --seed=1   # + randomized DOM/selector fuzzing
pnpm verify:page -- --out=/tmp/diff.html   # self-contained page, for the chrome-devtools MCP

The last command writes a page with both libraries and the harness inlined, so the same sweep can be driven from the chrome-devtools MCP (or a browser opened by hand):

// evaluate_script on the generated page
() => window.runSweep({ fuzz: 3000, seed: 42 })
// => { checks: 18828, realDifferences: 0, childCombinatorFixes: 14, verdict: 'IDENTICAL', ... }

Latest run: 4 seeds x ~19k comparisons (≈75k total), 0 real differences in real Chrome, covering nested/open/closed shadow roots, slots, light+shadow mixes, sibling and child combinators, quoted/bracketed attributes, pseudo-classes, CSS comments, scoped element roots, the cached-list third argument, plus empty and invalid selectors (which throw the same way). Both the playwright and the chrome-devtools-MCP driver produce identical numbers.

Every comparison falls into one of three buckets:

| bucket | expected | meaning | | --- | --- | --- | | real differences | 0 | a genuine behavioural regression | | > fixes | any | differences on selectors using > — the upstream bug below being fixed | | dedupe-only | any | the improvement described below |

Fixed: the child combinator >

The original re-tests a > group at every ancestor while climbing the composed tree, so > silently degrades into a descendant combinator. Given <ul class="c1"><li class="c1"><div class="c1"></div></li></ul>:

| querySelectorAllDeep('ul.c1 > .c1') | result | | --- | --- | | native document.querySelectorAll | [li] | | query-selector-shadow-dom | [li, div] | | this library | [li] |

a > b means "a is b's composed parent", so > also works across a shadow boundary: my-host > .panel matches the shadow root's child. If you are migrating and something stopped matching, > is the thing to check — the old behaviour was a b in disguise.

Other intentional differences, both safe improvements:

  1. Results are de-duplicated. The original can return the same element twice when it matches more than one comma-separated part (.a, .a). This library never does, matching querySelectorAll semantics.
  2. An unmatchable selector is rejected. Invalid selectors still throw the same SyntaxError/DOMException; only the wording of the message can differ (both come from the browser).

Everything else — including the remaining quirks — is reproduced on purpose: the composed result order, per-comma-part grouping, and never matching the boundary element itself when a search is scoped to an element.

Performance

pnpm benchmark runs both libraries against the same DOM in the same page (median of alternating rounds). Typical results on Chrome 153 (µs/op, lower is better):

| Scenario | original | modern | speedup | | --- | --- | --- | --- | | Deep mixed shadow & slot (querySelectorDeep) | 48 | 35 | 1.37x | | 7-level nested tree (querySelectorDeep) | 34 | 23 | 1.48x | | .class across 100 heavy shadow roots | 145 | 81 | 1.79x | | .metric-item across 100 roots (querySelectorAllDeep) | 354 | 199 | 1.78x | | Unique selector every call (parse-cache miss) | 195 | 76 | 2.57x | | Cross-boundary combinator + attribute | 51 | 15 | 3.40x | | Comma-separated multi-boundary (querySelectorAllDeep) | 10 | 2 | 5.00x | | Scoped element root | 14 | 9 | 1.56x | | Pure light DOM querySelectorAll | 138 | 83 | 1.66x |

Overall: 1.68x faster by sum, 1.66x geometric mean, faster on 17 of 19 measured cases, with the remaining cases at parity. The wins come from letting native querySelector(All) do the filtering (whole-selector fast path when there is no shadow DOM — now including > selectors — per-root pre-filter otherwise), a memoized selector parser, and a single tree walk instead of the original's two. Set ROUNDS=15 pnpm benchmark for a lower-noise run.

License

MIT