@uindow/css - The Smarter CSS Selector Generator
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.
Installation
npm
const { findOne, findAll } = require("@uindow/css");
Browser (standalone)
<script src="https://uindow.github.io/css/selector.js"></script> <script> const { findOne, findAll } = Uindow_CSS; </script>
Quick start
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 }, // ]
Why @uindow/css?
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.
Comparison with @medv/finder
| 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 |
Features
Multiple selectors, ranked by quality
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})`); });
Prefix and suffix attribute matching
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).
Clean, human-readable output
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-item
Sensible default filters
Unstable 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.
Fine-grained penalty model
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 |
Performance controls
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 });
Fuzziness
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) selectors
Effort
Controls 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 results
Compound selectors at every level
Most 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.
API
findOne(element, options?)
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 }
findAll(element, options?)
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, });
Configuration reference
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 });
License
MIT © 2026 Uindow™