Open-source project
fb55/css-select avatar
fb55/css-select

css-select compiles selectors into a stack of functions

a CSS selector compiler & engine

634 stars82 forksTypeScriptBSD-2-Clause

At a glance

What is it?
css-select is a BSD-2-Clause selector compiler and engine for JavaScript and TypeScript that parses a selector with css-what and evaluates it right to left, which is the design choice behind both its speed claims and its differences from the jQuery style engines it cites.
Who is it for?
css-select fits a Node project that has to query a parsed DOM tree, whether that tree comes from htmlparser2, from a scraped document or from a test fixture, and it fits a project that wants to compile a selector once and reuse the resulting function across many queries.
Can I use it commercially?
Yes. BSD-2-Clause 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 received new commits within the last day.
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

Why right-to-left execution is the whole point

Most CSS engines written in JavaScript execute selectors left to right, which the README says means every component of the selector runs in order from left to right. The example given is the selector a b: those engines first query for a elements, then search those for b elements. The README names Sizzle, Qwery and NWMatcher as engines that take that approach.

The problem is the cost. Children of an a are checked multiple times, first to see whether they are also a elements, and then once for every a above them to see whether they are b elements. In the README's big O notation that is O(n^(k+1)), where k is the number of descendant selectors.

The alternative is to look for the b elements first and then check whether they have a superior a element, which is O(n). The README calls this right-to-left execution and states that this is what css-select does, and why it is quite performant.

The reason to care is that this library is not trying to be a browser. It is a selector engine for a DOM tree held in memory, which is the situation in a scraper, a server side renderer, a static site generator or a test harness. In those settings you are usually running a lot of queries over a large tree, and the asymptotic difference between the two strategies is the difference between a query that scales and one that does not.

Parsing with css-what, then compiling to functions

The mechanism is described step by step, and the walkthrough of the selector a b is the clearest account of how the library works.

The first step is parsing, and the README points at the css-what module for that job. Parsing turns the selector into an array of building blocks:

js
[
    { type: "tag", name: "a" },
    { type: "descendant" },
    { type: "tag", name: "b" },
];

The README notes in passing that this array is actually wrapped in another array, which is where commas in selector lists come in.

The second step is compiling, and here the idea is to turn every part of the selector into a function that takes an element as its only argument. The function checks whether the passed element matches its part of the selector, and if it does, the element is passed to the next function representing the next part. An element accepted by every part of the selector matches it.

The third step is the traversal, which is where the right-to-left order shows up. Elements are passed from the rightmost part of the selector, b in the example, to the leftmost, a. For traversals such as the descendant operator between a and b, the implementation walks up the DOM tree starting from the element passed as the argument.

So the compiled artefact is a stack of small functions, and querying is the act of pushing candidate elements through that stack. The README is refreshingly honest about the documentation gap here, with a placeholder for a more in-depth description of the implementation and a joking instruction to build a spaceship.

Four entry points, and a mutation warning

The API is imported as a namespace:

js
import * as CSSselect from "css-select";

There are four functions. selectAll queries elements and returns an array containing all matches, and it is also the default export. compile compiles the query and returns a function, which is the entry point to use when you will run the same selector many times. is tests whether a single element is matched by a query. selectOne takes the same arguments as selectAll and returns the first match, or null if there was no match.

The query argument of selectAll, is and selectOne can be either a CSS selector string or a function, so a compiled selector can be passed straight back in.

Two behaviours are documented in bold because they will bite. The library throws errors when invalid selectors are passed to it, which the README says is done to aid with writing CSS selectors but can be unexpected when processing arbitrary strings. And when you pass an array of elements, that array may be mutated, because elements that are subsets of other elements will be removed from it. Passing a single element instead queries its children.

That mutation is the one to design around. A caller that keeps a reference to the array of candidates and reuses it will find it shrinking.

Options that change what a selector means

Every option is optional, and several of them change semantics rather than performance.

xmlMode makes tag names case-sensitive, and it defaults to false. context is the context of the current query, used to limit the scope of searches and matched directly with the :scope pseudo-class. relativeSelector decides whether selectors are relative to the context, which by default means that no parent elements of the context are matched, with the example that a b c with context b will never give any results, and setting it to false stops selectors being absolutized so they can test for parent elements outside the context.

rootFunc is the last function in the stack, called with the last element that was looked at, which is the hook for collecting results yourself. pseudos is a map of pseudo-class names to functions or strings, and it is how you add a custom pseudo-class. adapter selects the adapter used to talk to the backing DOM structure, with domutils as the default.

cacheResults is the option with a footgun. It allows css-select to cache results for some selectors, which the README says can greatly improve querying performance, and it defaults to true. The advice is to disable it if the document can change in between queries with the same compiled selector. That is exactly the situation a long-lived server process hits: a compiled selector held in a module scope, a document mutated by a request, and a cached answer from before the mutation.

Adapters are how it queries something other than htmlparser2

In its default configuration css-select queries the DOM structure of the domhandler module, also known as the htmlparser2 DOM. Querying alternative DOM structures is handled through the adapter option, and the README has a dedicated section for it.

A custom adapter must match the Adapter interface, which is defined in the project's own types file. The division of labour is spelled out: tree traversal, the findAll and findOne operations, is handled internally by css-select using the adapter's primitive methods, so an adapter only needs to implement basic operations such as isTag, getChildren and getParent.

That is a sensible split. The engine owns the algorithm, which is the part that has to be correct and fast, and the adapter owns the tree access, which is the part that differs between DOM implementations. Writing an adapter is therefore a matter of describing your tree in a dozen methods rather than reimplementing selector matching.

The same reasoning explains the project's dependency list. domhandler and domutils handle the default tree, css-what does the parsing, and boolbase and nth-check handle the boolean logic and the an+b micro-syntax used by nth-child and friends. The parsing, the tree and the two small helpers are separate modules, so the engine itself is thin.

The package metadata and a major version that broke the module format

The package is css-select, version 7.0.0, published as ES modules only, since the type field is module and the exports map points at the compiled index and its type declarations. The files array is interesting: it ships both dist and src, while excluding the spec files, so the TypeScript sources travel with the package.

The runtime dependencies are five modules:

json
    "dependencies": {
        "boolbase": "^2.0.0",
        "css-what": "^8.0.0",
        "domhandler": "^6.0.1",
        "domutils": "^4.0.2",
        "nth-check": "^3.0.1"
    },

The author is Felix Boehm, and the package is marked sideEffects false, which lets a bundler drop it when nothing from it is used, and the funding field points at GitHub Sponsors.

The version history shows the format change: v7.0.0 on 2026-03-22, v6.0.0 on 2025-06-29 and v5.2.1 on 2025-06-28. A library that shipped CommonJS for years and then moved to ES modules is a common upgrade hazard, since a project still on require cannot consume the new major without a bundler or a dynamic import workaround.

The development tooling is current and doubled up on linters, with eslint, typescript-eslint and biome all configured, vitest for the test runner, and the build itself is just tsc. The typescript dependency is at ^6.0.3, which places the project on a very recent toolchain.

The test suites are borrowed from the engines it replaces

The most telling line in the feature list is about test coverage: high test coverage, including the full test suites from Sizzle, Qwery and NWMatcher.

That is a compatibility strategy rather than a quality claim. Those three engines are the left-to-right engines the README uses as its motivating example, and each of them has a large body of selector tests. Running their suites against css-select is a strong statement about which selectors are expected to work, including the awkward ones that only exist to preserve historical behaviour.

The remaining feature list is worth reading carefully. There is a full implementation of CSS3 selectors and most CSS4 selectors, and a partial implementation of the jQuery and Sizzle extensions, with the README pointing at cheerio-select for the remaining selectors. That is a real division of labour between two projects, and it is the closest thing to an alternative in the documentation: cheerio-select covers the jQuery extensions that css-select does not.

The release dates tell you the project is actively maintained. The repository is not archived and the last push was on 2026-10-20, months after the v7.0.0 release in March 2026 and following a v6.0.0 in June 2025.

Editorial conclusion

css-select fits a Node project that has to query a parsed DOM tree, whether that tree comes from htmlparser2, from a scraped document or from a test fixture, and it fits a project that wants to compile a selector once and reuse the resulting function across many queries. It does not fit a browser page, where the native querySelectorAll is already right to left and native, and it does not fit a codebase that needs the remaining jQuery extensions, since the README points at cheerio-select for those. Before adopting it, decide whether you want the default domhandler DOM or need a custom adapter, and turn cacheResults off if the document changes between queries with the same compiled selector.

Frequently asked questions

What does css-select do that a browser's querySelectorAll does not?

css-select works on a DOM tree held in memory, such as the htmlparser2 DOM built by domhandler, and it exposes a compiler, so a selector can be turned into a reusable function. In a browser page the native querySelectorAll already runs right to left and needs no compilation.

How do I use css-select with a DOM other than htmlparser2?

You pass an adapter through the adapter option. A custom adapter must match the Adapter interface, and only needs to implement primitive operations such as isTag, getChildren and getParent, because css-select handles the tree traversal itself.

Does css-select throw when I pass an invalid selector?

Yes. The documentation states that css-select throws errors when invalid selectors are passed to it, which aids writing selectors but can be unexpected when processing arbitrary strings.

What is the difference between selectAll, selectOne, compile and is?

selectAll returns an array of all matches and is the default export, selectOne returns the first match or null, compile returns a function for the compiled query, and is tests a single element against a query. A query can be a selector string or a function in all of them.

Which jQuery selectors are not supported by css-select?

Only a partial implementation of the jQuery and Sizzle extensions is provided. The documentation points to cheerio-select for the remaining selectors.

Official sources

  1. fb55/css-select on GitHub
  2. License: BSD-2-Clause
  3. Project website
  4. README
  5. Releases
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/fb55-css-select.svg)](https://hysenlabs.com/projects/fb55-css-select)

Was this useful?