finder
Generates the shortest, unique, and robust CSS selector for any DOM element in just 1.5kb.
Repository Health
Technical Analysis
@medv/finder is a zero-dependency CSS selector generator for the browser. Given any DOM element, it searches upward through ancestors and combines IDs, classes, attributes, and tag names to build the shortest selector string that uniquely matches that single node on the page, falling back to nth-of-type paths when nothing shorter is unique.
It ships as a single ~1.5kb (minified & gzipped) ESM module with a fully configurable matching strategy — you can restrict which class names, attribute names/values, and IDs are considered safe to use in a selector, cap the search with a timeout, and tune how aggressively the resulting selector gets optimized for brevity. It’s the selector-generation engine behind several session-replay, click-tracking, and browser-automation recorder tools.
What You Get
- Shortest unique CSS selector generation for any element, with automatic fallback to nth-of-type paths when no shorter selector is unique
- Fully pluggable matching rules for id, class, tag, and attribute names via idName/className/tagName/attr predicate functions
- Configurable root scope, search timeout, and path-check limits so selector generation never blocks the main thread for long
- Zero runtime dependencies and a ~1.5kb minified+gzipped bundle size
- Shadow DOM aware root resolution for selectors generated inside web components
Common Use Cases
- Click and event tracking - product analytics tools generate a stable CSS selector for the exact element a user clicked so events can be aggregated or replayed later
- Session replay recorders - browser extensions and SDKs that record user sessions use finder to describe which element on the page an action targeted
- Browser automation test generators - codegen-style tools that record a user’s interactions and emit selector-based test scripts
- Heatmap and click-map tooling - tools that aggregate recorded interactions need a reliable selector to bucket events by element rather than raw coordinates
Under The Hood
Architecture
The module is a flat, functional pipeline in a single file (finder.ts, ~385 lines) with no class hierarchy or DI: tie() scores an ancestor’s id/class/attribute/tag-name/nth-child candidates with a penalty value, search() walks up the tree building per-level candidate sets and lazily yields sorted path combinations via a combinations() generator, unique() validates a candidate against the live DOM with querySelectorAll, and a separate optimize() generator then tries removing intermediate path segments from a working selector while it stays unique and still resolves to the original element. A fallback() path handles the timeout case with an nth-of-type-only selector. Because everything lives in one file with no internal layering, a change to the Knot shape or penalty scoring ripples directly through every function that touches it.
Tech Stack
Written in TypeScript (5.7.2), compiled with plain tsc (no bundler), published as ESM-only (type: module) with zero runtime dependencies — devDependencies are limited to css.escape (a CSS.escape polyfill used in the test environment), jsdom and vitest for testing, prettier for formatting, and typescript for the build. It’s dual-published to npm (compiled finder.js/finder.d.ts) and JSR (jsr.json points straight at the TypeScript source). Two GitHub Actions workflows run a Node 20/22 test matrix and a prettier --check formatting gate — there is no linter and no bundler tooling.
Code Quality
Tests in tests/finder.test.js use vitest + jsdom and snapshot-test the selectors generated across several real-world captured HTML pages (github.com, stripe.com, deployer.org, tailwindcss.com, a shadow-DOM fixture), asserting every generated selector both selects exactly one node and resolves back to the originating element — a strong end-to-end correctness check, though there are no isolated unit tests of internal helpers like tie(), combinations(), or optimize(). The code is fully typed (an explicit Options type, typed Knot records, no observed any), and errors from invalid input or an unresolvable selector are raised explicitly with throw new Error(...) rather than swallowed. Style is enforced only via prettier in CI; there is no ESLint configuration.
API Design
The entire public surface is one function, finder(element, options?), with sensible defaults for every option (root, timeoutMs, seedMinLength, optimizedMinLength, and the four naming predicates) so the zero-config call finder(event.target) works immediately. Power users can override just the idName/className/tagName/attr predicates without needing to understand the internal Knot/penalty model, and the library exports its default predicate functions individually so callers can wrap and extend rather than fully replace them — a pattern the README documents with copy-pasteable examples. Documentation is limited to the README (no separate docs site or generated API reference), but its option table covers every config field and default.
Used by 2 apps in this directory
Chaskiq
CRM · Customer Support
Self-hosted live chat, video calls, help center, and marketing automation — a full-stack Intercom alternative you run on your own infrastructure.
OpenReplay
Analytics
Self-hosted session replay and product analytics suite that lets you see exactly what users do on your web app — without sending data to third parties.