Generate one - or many - unique CSS selectors for any DOM element. Clean, human-readable output. A configurable penalty model that ranks results from most semantic to most specific. Built for performance, designed to stay out of your way.
Works everywhere JavaScript runs: Node.js, browsers, and any environment with a DOM.
npm
npm install @uindow/cssconst { findOne, findAll } = require("@uindow/css");Browser (standalone)
<script src="https://uindow.github.io/css/selector.js"></script>
<script>
const { findOne, findAll } = Uindow_CSS;
</script>const { findOne, findAll } = require("@uindow/css");
// The single best selector
const { selector, penalty } = findOne(document.querySelector(".hero"));
// → '[data-id="42"] .hero'
// Every viable selector, ranked from best to most specific
const results = findAll(document.querySelector(".hero"), { maxResults: 5 });
// → [
// { selector: '[data-id="42"] .hero', penalty: 1.3 },
// { selector: '.hero', penalty: 1.3 },
// { selector: 'section [data-id="42"] .hero', penalty: 2.6 },
// { selector: 'main .hero', penalty: 2.75 },
// { selector: 'main .hero:nth-of-type(1)', penalty: 4.4 },
// ]Most CSS selector generators treat the problem as a lookup: walk up the DOM, find something unique, done. One selector, take it or leave it.
@uindow/css treats it as a search problem. It explores the space of possible selectors across every ancestor level, scores each candidate against a configurable penalty model, prunes aggressively to stay fast, and surfaces a ranked list of results - so you always get the shortest, most readable, most stable selector first, with fallbacks already computed.
| Feature | @uindow/css | @medv/finder |
|---|---|---|
| Custom root element | ✅ Any HTMLElement, Document, or ShadowRoot |
✅ Supported |
| ID filter | ✅ idFilter filter |
✅ idName filter |
| Tag filter | ✅ tagFilter filter |
✅ tagName filter |
| Class filter | ✅ Excludes is-*, has-*, js-*, css-* by default |
|
| Attribute filter | ✅ Excludes style, width, height, URLs, values ≥ 32 chars by default |
|
| Search timeout | ✅ timeout with graceful fallback |
|
| Returns multiple selectors | ✅ Up to maxResults, ranked by penalty |
❌ Single selector only |
| Per-type penalty tuning | ✅ idPenalty, tagPenalty, attrPenalty, attrMatchPenalty, classPenalty, nthOfTypePenalty, nthChildPenalty, lengthPenaltyThreshold |
❌ Not configurable |
| Candidate/path caps | ✅ maxCandidatesPerLevel, maxPathsPerLevel, maxPathsTotal |
❌ Not supported |
| Prefix/suffix attribute matching | ✅ [attr^="start"], [attr$="end"] |
❌ Not supported |
| Human-readable attribute selectors | ✅ Always emits [attr="123"] |
❌ Uses CSS.escape() |
| Fuzziness | ✅ Trade exclusivity for shorter selectors (0% to 100%) |
❌ Not supported |
| Effort | ✅ Trade processing time for shorter selectors (0% to 100%) |
❌ Not supported |
| Compound selectors | ✅ Attempts to merge tag, classes, and attributes at each level: input.check[type="checkbox"][value="2"] |
❌ Simple selectors only |
findAll() returns up to maxResults selectors sorted by ascending penalty - from the most semantic to the most specific. Pick the best one programmatically, or hand the whole list to your users.
const { findAll } = require("@uindow/css");
const results = findAll(el, { maxResults: 10 });
results.forEach(({ selector, penalty }) => {
console.log(`${selector} (penalty: ${penalty})`);
});When an exact attribute match would be brittle or unavailable, @uindow/css can generate selectors using the CSS ^= (prefix) and $= (suffix) operators. They carry their own separate penalty so they only surface when genuinely useful.
/* Exact match - always preferred */
[data-id="42"]
/* Prefix match - used when it uniquely identifies the element */
[data-id^="prod-"]
/* Suffix match */
[data-id$="-active"]Tune how eagerly these appear with attrMatchPenalty (default 1.2).
Every selector is emitted in the plain [attr="value"] format. No CSS-escaped sequences, no surprises when you read, copy, or paste the result.
/* ✅ @uindow/css */
[data-id="123"] .nav-item
/* ❌ Other tools */
[data-id="\31 23"] .nav-itemUnstable and noisy parts of the DOM are excluded before the search begins:
- Attributes -
style,width,height, URL values, and anything over 32 characters are ignored by default. - Classes -
is-*,has-*,js-*, andcss-*are ignored by default. These are state and behaviour hooks - they change at runtime and make brittle selectors. - IDs - all IDs are allowed by default. Provide your own
idFilterto exclude auto-generated or dynamic ones.
Every filter is a plain function, so your own rules are one line of code.
Each selector type has an independent penalty weight. Lower means it shows up more often in results; higher means it only appears when nothing better is available.
| Option | Default | Controls |
|---|---|---|
idPenalty |
1 |
#id selectors |
tagPenalty |
1.1 |
div, span, … |
attrPenalty |
1.25 |
[name], [value="1"] |
attrMatchPenalty |
1.2 |
[name^="x"], [value$="5"] |
classPenalty |
1.3 |
.button, .nav-item, … |
nthOfTypePenalty |
3 |
div:nth-of-type(2) |
nthChildPenalty |
6 |
:nth-child(3) |
lengthPenaltyThreshold |
32 |
Extra penalty for selectors longer than N chars |
For large or deeply nested DOMs, hard caps keep the search bounded:
const { findAll } = require("@uindow/css");
findAll(el, {
maxCandidatesPerLevel: 2500, // Candidates evaluated per ancestor level
maxPathsPerLevel: 50, // Unique paths kept per level
maxPathsTotal: 1000, // Unique paths across the entire search
timeout: 1500, // Return best found so far after N ms
});By default, @uindow/css only returns selectors that match the target element exclusively. Set fuzziness > 0 to allow selectors that match the target first while potentially matching other elements too - trading strict uniqueness for shorter, cleaner output.
findAll(el, { fuzziness: 0 }); // Strict - only exclusive selectors (default)
findAll(el, { fuzziness: 5 }); // Up to 5% non-exclusive matches allowed
findAll(el, { fuzziness: 20 }); // Relaxed - prioritise brevity
findAll(el, { fuzziness: 100 }); // Fuzzy - only non-exclusive (first-match) selectorsControls how much effort is spent optimizing partial results before returning the final list. This setting trades speed for result quality: higher values spend more time exploring candidates before producing the final result set.
findAll(el, { effort: 0 }); // Low effort - quickly returns the first `maxResults`
findAll(el, { effort: 50 }); // Medium effort - evaluates 25 × `maxResults` candidates before returning the final results
findAll(el, { effort: 100 }); // Maximum effort - evaluates 50 × `maxResults` candidates before returning the final resultsMost CSS selector generators pick a single token per ancestor level - a class, or an attribute, or a tag - and move on. This produces selectors that are either fragile (too generic) or bloated (too many levels deep).
@uindow/css builds compound selectors at each level by default, combining the tag, relevant classes, and attributes into a single, precise token:
/* ✅ @uindow/css - compound, precise, readable */
input.check[type="checkbox"][value="2"]
/* ❌ Other tools - simple, one token at a time */
input
.check
[type="checkbox"]The result is selectors that are shorter, more stable, and immediately understandable at a glance.
Returns the single best selector for element, or throws if none can be generated.
const { findOne } = require("@uindow/css");
const { selector, penalty } = findOne(document.getElementById("main"));
// → { selector: '#main', penalty: 1 }Returns up to maxResults selectors sorted by ascending penalty, or throws if none can be generated.
const { findAll } = require("@uindow/css");
const results = findAll(document.getElementById("main"), {
maxResults: 10,
});All configuration items are optional. Any omitted option falls back to its default.
const { findAll } = require("@uindow/css");
findAll(el, {
// Scope - nothing above this element appears in any selector
root: document.querySelector("#app"), // Default: document.documentElement
// Filters - return false to exclude a candidate from the search
idFilter: (name) => !name.startsWith("auto-"),
tagFilter: (name) => name !== "div",
classFilter: (name) => !name.startsWith("is-"),
attrFilter: (name, value) => name.startsWith("data-") && value.length < 32,
// Penalties - lower value → type appears more often in results
idPenalty: 1,
tagPenalty: 1.1,
attrPenalty: 1.25,
attrMatchPenalty: 1.2,
classPenalty: 1.3,
nthOfTypePenalty: 3,
nthChildPenalty: 6,
lengthPenaltyThreshold: 32,
// Performance caps
maxCandidatesPerLevel: 2500,
maxPathsPerLevel: 50,
maxPathsTotal: 1000,
maxResults: 25,
timeout: 1500,
// Percentage of shorter, first-match selectors
fuzziness: 0,
// Effort spent on optimizing candidates
effort: 50
});MIT © 2026 Uindow™