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

semantic-selector

v0.1.1

Published

Generate a semantic, identity-only CSS selector for a DOM element — ranks each ancestor by meaningful identity (id/url/name/class/ARIA), never by position; resolves ambiguity out-of-band rather than via positional ordinals

Readme

A semantic selector (not a unique one)

This project aims to generate a semantic or 'meaningful' CSS selector for a DOM element. We prefer to use good class names, ids, and certain attributes over DOM position / structure to give a greater chance that the selector will still point to the same element(s) even if the DOM content is shifted around and the surrounding page has been updated over time. The library is picky and detects and demotes common framework-generated and presentational classes which are assumed to be more often swapped in and out to change the appearance or position of an element.

It does not guarantee that it will generate a unique selector across all the elements on the page.

Selector Uniqueness: on-page vs. between page versions

Selector uniqueness is often achieved in other libraries with positional ordinals (:nth-of-type, :nth-child) and/or over specification of DOM structure (div > span > div).     This library deliberately avoids that, and instead focuses only on generating a good selector. The uniqueness constraint means that the choice of selector is dictated by other elements which may only be present in the current version of the page. Other libraries have to continue iterating, adding incidental noise to fabricate a distinction in order to satisfy the uniqueness constraint.

Instead we let the calling code decide on how to distinguish between multiple elements if required, e.g. by also recording element dimensions, or by recording that the target element is the 2nd on the page (a global 'nth' positional in terms of document.querySelectorAll instead of a brittle local 'nth' somewhere in the selector).

Install

npm install semantic-selector

Usage

import { semanticSelector } from 'semantic-selector';

const el = document.querySelector('.buy-button')!;
semanticSelector(el); // relative to document.body (default)
semanticSelector(el, someRoot); // relative to a given root
semanticSelector(el: Element, root: Element = document.body): string

Browser (UMD global semanticSelector):

<script src="https://unpkg.com/semantic-selector"></script>
<script>
  semanticSelector(document.querySelector('#target'));
</script>

What you get back

semanticSelector returns a plain string — the selector. Given the following markup (two identical buy buttons):

<main>
  <section class="wp-block-group product-card">
    <a class="wp-block-button__link buy-button" href="/checkout?utm_source=x">Buy now</a>
  </section>
  <section class="wp-block-group product-card">
    <a class="wp-block-button__link buy-button" href="/checkout?utm_source=y">Buy now</a>
  </section>
</main>
const el = document.querySelectorAll('.buy-button')[1];
semanticSelector(el);
// → '.product-card a[href^="/checkout"]'

Note what happened: the framework classes (wp-block-group, wp-block-button__link) and the volatile ?utm_source=… query were dropped, the semantic .product-card ancestor was kept, and — because both buttons in this toy example can be considered to have the same identity — the result deliberately matches both (no:nth-of-type inserted to force uniqueness).

Since the selector is not guaranteed unique, the caller resolves any residual ambiguity out-of-band by pairing it with a match index + count (see Selector Uniqueness):

const selector = semanticSelector(el);
const matches = Array.from(document.querySelectorAll(selector));

const result = {
  selector, //                  → '.product-card a[href^="/checkout"]'
  selectorMatchIndex: matches.indexOf(el), // → 1  (0-based position among matches)
  selectorMatchCount: matches.length, //       → 2  (total elements this matches)
};

To relocate the element later, re-run the selector against the new page and take the element at selectorMatchIndex (optionally cross-checked against recorded geometry).

How it ranks identity

Per element, best → worst:

  1. a strong own id (stops the walk)
  2. urlhref / src, with volatile query/hash stripping
  3. a form control's name — the backend submission key
  4. class and ARIA, interleaved by quality: tier-A class > aria-label > tier-B class > role > tier-C class > rel
  5. a stable id in the element's subtree — :has(#id)
  6. a stable id on the immediately preceding sibling — #prev + tag

Class quality tiers: A = semantic/component (entry-content, product-card), B = framework-namespaced (wp-…, elementor…, Mui…), C = utility/atomic (Bootstrap grid, spacing helpers). The best-tier class is chosen (not the first in DOM order), and a low-quality class loses to an explicit aria-label.

Structural noise is dropped. Only the clicked element keeps its tag; ancestor tags are stripped (#nav a[href="/x"], not nav#nav > ul > li > a…). Ancestors with no identity are omitted entirely, and a redundant low-quality class ancestor (one whose removal doesn't grow the match set) is pruned — so semantic context and strong-id anchors survive, framework wrappers don't.

Generated values are rejected for both ids and classes: ember, React useId, Radix, MUI, Headless UI, Angular Material/CDK, uuid/hex hashes, styled-components, emotion, CSS-module hashes, state classes, and over-long identifiers.

Comparison

finder is the best-in-class jumping off point for this library; it has as it's main goals uniqueness and brevity, both important, but not what we're aiming for here.

stable-selector addresses the same problem but is still strongly weighted towards finding a unique selector in the current document, whereas semantic-selector aims to produce a selector which will still point to the same element in future versions of the document. semantic-selector deliberately avoids structure and position.

| | semantic-selector | finder (antonmedv) | stable-selector (qaz1230sp) | | --------------------------- | ------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------------------------------------------- | | Goal | Semantic, long term 'identity' of an element between page versions | Shortest unique selector | Unique, stable selector | | Selection | Fixed-priority ladder | Penalty search for shortest unique | 4-dimension weighted scoring (uniqueness 0.4, stability 0.35, brevity, readability) | | Selector on-page uniqueness | Not required → caller computes matchIndex + count out-of-band | Required — keeps searching | Required — scored down; structural fallback forces it | | Positional ordinals | never in selector itself (see matchIndex) | :nth-child when needed | :nth-of-type when needed | | Combinators | Descendant | Descendant | Direct child > | | Ancestor structure | Identity-only; tags stripped; redundant low-quality pruned | Minimal unique path | Path up to maxDepth, nth-enriched | | Value filtering | Reject-lists for known frameworks | wordLike (rejects digits/short names) | 3 layers: built-in patterns + Shannon-entropy heuristic + user blacklist | | Class quality | A/B/C semantic tiers, best chosen | first N matching classes | stable classes (up to 3), no semantic tier | | Output | CSS | CSS | CSS + XPath + Playwright | | Config | (el, root) only | predicates + threshold | extensive (configure(), priorities, blacklist, formats, maxDepth) |

The trade-off is about when the selector is used:

  • finder / stable-selector optimise for a unique locator against the DOM in front of you now (scraping, a Playwright test run). Uniqueness is paramount, and nth is fine because the DOM won't move under you mid-session.
  • semantic-selector optimises for drift between capture and retrieval. Because out-of-band matchIndex can resolve ambiguity, we can let go of the uniqueness requirement and produce a more semantic selector that has a higher likelihood of surviving long term page restructuring or even a swap out of the framework used to produce the page.

License

MIT © Eoghan Murray