Library / SDK
fczbkk/css-selector-generator avatar
fczbkk/css-selector-generator

css-selector-generator turns a DOM node into a selector you can paste

JavaScript object that creates unique CSS selector for given element.

596 stars90 forksTypeScriptMIT

At a glance

What is it?
css-selector-generator is a TypeScript library that produces a unique CSS selector for a DOM element, with options for selector types, root scope, blacklists, whitelists, shadow roots and multi-element targets. It is published as one npm package from a monorepo whose readme is generated.
Who is it for?
css-selector-generator fits frontend and test tooling work where a human or a script needs to point at a specific element in a page, and where the selector has to be short, readable and unique rather than a full nth-child chain.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 77 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One function for a selector, one for every selector

css-selector-generator is a JavaScript object that creates a unique CSS selector for a given element, and the library description adds that it supports multiple elements, Shadow DOM and configurable selector types.

There are two entry points, and the difference between them is the shape of the answer. getCssSelector returns the first selector it finds, and cssSelectorGenerator returns a generator that yields all possible selectors, starting from the simplest and ending with the fallback selector. For a simple document with a single classed div, the first is ".myElement" and the second yields ".myElement", then "div.myElement", then "body .myElement".

That ordering is the design decision worth understanding. A generated selector is only useful if it is short, so the library starts from the most specific and cheapest option and degrades as needed, and a caller who wants to see the trade-off can iterate the generator and choose.

The typical use is exactly what you would guess: attach a click listener to the document, take event.target, and log the selector for the element the user clicked. The readme spells out that sequence in a few lines, and it is the case the library is designed around.

There is also a sandbox linked from the readme where you can write HTML and see the selectors the library produces, which is the fastest way to understand how the candidate ordering behaves.

Options decide what a candidate selector may look like

The options list is long, and each entry changes what the generator is allowed to produce, which makes the output a policy decision rather than a single answer.

selectors sets which selector types are in play, with class, id and tag named in the TypeScript example. root sets the element the search stops at, with document.body used in the example, and use scope is a separate option for restricting to a scope. blacklist and whitelist filter which classes and ids may be used, so a generated class pattern such as ".ignore-*" can be excluded outright. ignoreGeneratedClassNames excludes classes that look machine made. includeTag adds the tag name to the selector, turning ".myElement" into "div.myElement".

Then there are the three limits. maxCombinations caps how many candidate combinations are tried, maxCandidates caps how many candidates are examined, and maxResults caps how many selectors the generator yields. combineWithinSelector and combineBetweenSelectors control whether selectors may be combined into one comma separated rule.

Set against those, the TypeScript example in the readme shows the whole shape of a configuration.

typescript
import { getCssSelector, cssSelectorGenerator } from "css-selector-generator";
import type {
  CssSelectorGeneratorOptionsInput,
  CssSelectorType,
} from "css-selector-generator/types/types.js";

const options: CssSelectorGeneratorOptionsInput = {
  selectors: ["class", "id", "tag"],
  blacklist: [".ignore-*"],
  root: document.body,
};

const selector = getCssSelector(targetElement, options);

The types are importable from a dedicated types entry point, and the library is written in TypeScript with type definitions included.

Shadow DOM gets a selector relative to the shadow root

Shadow DOM support is one of the features the description leads with, and the mechanism is worth being precise about.

When you hand the library an element that lives inside a shadow tree, it infers the shadow root itself and generates a selector relative to that root, rather than trying to reach across the shadow boundary. The readme shows the element being created with attachShadow in open mode, and the resulting selector being just ".shadowElement".

You can also specify the root explicitly with the root option, which gives the same result.

javascript
const shadowRoot = element.attachShadow({ mode: "open" });
const shadowElement = shadowRoot.appendChild(document.createElement("div"));
shadowElement.className = "shadowElement";

// Automatically detects shadow root
getCssSelector(shadowElement);
// ".shadowElement"

// Or explicitly specify the shadow root
getCssSelector(shadowElement, { root: shadowRoot });
// ".shadowElement"

The boundary here is worth stating, because it is the kind of thing that only shows up in production. A selector that is relative to a shadow root is not usable from outside that root, so the value of the feature depends on where the selector is going to be evaluated. If it is going back into a test runner or a query inside the same component, the short form is exactly what you want. If it is going to a document level query, it is not, and no option in the library changes that.

The example also uses open mode, and the readme does not describe behaviour for a closed shadow root.

When nothing unique exists, the fallback gets ugly on purpose

The fallback behaviour is documented in detail, and it is the part that tells you what you are getting when the library cannot do better.

getCssSelector determines the shortest CSS selector for the parent to child relationship, from the input element up to the root element. If there is no unique selector available for any of those relationships, a wildcard is used for that relationship, so a selector can end up looking like "#wrapper > * > div > .text".

And that selector may still not be unique, for example "#wrapper > * > div > *". In that case the library falls back to an entire chain of nth-child selectors, producing something like ":nth-child(2) > :nth-child(4) > :nth-child(1) > :nth-child(12)".

So the output has three tiers: a real selector, a selector with wildcards, and a positional chain. The first two are readable and reasonably stable. The third is correct but brittle, since it depends on the exact position of the element among its siblings, and it breaks the moment a sibling is inserted.

The practical implication is that a generated selector is a debugging and instrumentation convenience, not a test fixture. If you are writing assertions for a test suite, a data attribute in the markup will be more stable than anything this library can derive from a DOM that is already built.

Multiple elements share one selector when they can

The multi-element mode is a small feature with a useful property: it tries to find a single selector that matches all the targets.

You call the same function with an array of elements instead of one. If the elements share a class, the shared class is the answer, so two elements with class "aaa bbb" and "bbb ccc" give ".bbb".

javascript
getCssSelector([firstElement, secondElement]);
// ".bbb"

When no single selector covers them, a standalone selector is generated for each element and the results are combined into one comma separated rule.

javascript
getCssSelector([firstElement, secondElement]);
// "div, span"

This is the behaviour you want when you are collecting a set of elements to act on together, for example every matching node in a region of the page, and a comma separated selector is exactly the form a querySelectorAll call needs.

It is worth noting what the shared-class answer implies. The library is not merely choosing the shortest unique path, it is solving a set problem, and it will happily return a class that matches more elements than the two you asked about. That is a reasonable trade for a selector meant to be applied to a group, and a reason to look at the result before you rely on it.

It is a browser library, and in Node it needs a DOM

The library works on elements, so it needs a document to work against, and the readme is explicit about what that means for server side use.

For use without a package manager, you can download the library from the build folder and insert it into an HTML document directly, in which case the API is wrapped in a namespace.

html
<!-- link the library -->
<script src="build/index.js"></script>
<script>
  CssSelectorGenerator.getCssSelector(targetElement);
</script>

For Node, usually in tests, the instruction is not to require the library directly into the Node process, because there is no window object and no elements to select. Instead the library has to be added to the virtual window object, with a pointer to the JSDOM wiki page explaining how not to stuff JSDOM globals onto the Node global.

That is a real constraint on how the library is tested and used. A tool that needs a JSDOM instance to run is a tool for test environments and browser code, not for a server rendering HTML, and the readme's wording about not requiring it directly is a warning that the obvious approach fails.

The installation itself is unremarkable, which is a virtue for a library that ends up in other people's dependency trees.

shell
npm install css-selector-generator
# or
yarn add css-selector-generator

A monorepo whose readme is generated from scenarios

The repository is a monorepo with npm workspaces over packages/, and its own scripts tell you how the documentation and the code stay in step.

The build runs the build script of the css-selector-generator workspace, tests run across all workspaces, and there are lint and lint:build variants. The generate script has two parts: it regenerates the scenarios, which live in a css-selector-generator-scenarios workspace, and then regenerates the package readme through scripts/generate-package-readme.js. A generate:check script runs the same generation and then fails if the git diff on the generated scenario file and the package readme is not empty, which is a continuous integration guard against documentation drifting from the code.

So the readme you read in the package directory is produced output, and the examples in it come from a scenario suite rather than from prose written by hand. That is a good pattern for a library whose documentation is largely made of code samples, because it means the samples cannot silently rot.

There is also a release script, a build:pages script for the linked benchmark and sandbox pages, husky for git hooks, and a Prettier configuration. The package metadata in the root is for the monorepo itself, marked private and typed as an ES module, with the MIT licence and the author recorded as Riki Fridrich.

The licence is MIT, the repository is not archived, and the last push was on 2026-08-07, with no GitHub releases, so the npm version is the version to track rather than a tag.

Editorial conclusion

css-selector-generator fits frontend and test tooling work where a human or a script needs to point at a specific element in a page, and where the selector has to be short, readable and unique rather than a full nth-child chain. It is the wrong tool when the element is only reachable at runtime from a shadow tree you do not control, or when you need a stable selector across a re-render, because a generated selector is derived from the current DOM rather than from anything you put in the markup. Check first that the element has a class or id you can rely on, that the root option matches the scope you are querying within, and that your build tooling handles the ES module build that the readme describes.

Frequently asked questions

How do I get a CSS selector for an element with css-selector-generator?

Import getCssSelector and pass the element reference; it returns the first selector found, such as ".myElement". The cssSelectorGenerator function returns a generator of all possible selectors, simplest first, ending with the fallback selector.

Does css-selector-generator work with Shadow DOM?

Yes. When the element is inside a shadow tree the library infers the shadow root and returns a selector relative to it, such as ".shadowElement", and you can also pass the root explicitly with the root option.

What happens when no unique CSS selector exists?

The library falls back to a wildcard star for that parent to child relationship, and if the result is still not unique it falls back to a chain of:nth-child selectors such as ":nth-child(2) >:nth-child(4) >:nth-child(1) >:nth-child(12)".

Can I use css-selector-generator without npm?

Yes. Download it from the build folder and include the script in your HTML document, where the API is wrapped in the CssSelectorGenerator namespace. In Node you need a virtual DOM such as JSDOM, since the library requires a window object and elements to select.

Official sources

  1. fczbkk/css-selector-generator on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fczbkk-css-selector-generator.svg)](https://hysenlabs.com/projects/fczbkk-css-selector-generator)

Was this useful?