css-selector-parser
css-selector-parser turns a CSS selector string into a typed abstract syntax tree, lets you walk or construct that tree, and renders it back to selector text. Version 3 understands predefined CSS1 through Selectors Level 4 grammars, optional modules such as nesting and Shadow DOM selectors, and a progressive mode for unknown syntax. It parses selector syntax only; it does not query a DOM, calculate specificity, parse full stylesheets, or rewrite CSS files for you.
A strong focused parser for tools that need modern selector syntax, a typed AST, and round-trip rendering without bringing in PostCSS. Avoid it when your actual job is DOM matching, specificity calculation, or lossless stylesheet editing.
Use it if
- You are building a linter, codemod, analyzer, or editor feature that needs a structured selector AST
- You need to accept a particular CSS level or opt into modules such as nesting, scoping, position, or shadow parts
- You want the same dependency-free package in ESM, CommonJS, browser, and Node code
- You need to build selectors programmatically and render correctly escaped selector text
- You need to query HTML elements; this package parses syntax and does not provide querySelector-style matching
- You are writing a PostCSS transform; postcss-selector-parser fits PostCSS node mutation and processor workflows more directly
- You need CSS specificity scores; the documented exports are createParser, render, ast, and traverse, with no specificity calculator
- You are upgrading code written for version 1 or 2 without budget for migration; version 3 replaced CssSelectorParser with createParser and substantially changed AST node names and fields
- You must preserve source formatting byte for byte; rendering produces valid selector text from the AST, not a lossless concrete syntax tree with original whitespace and escape choices
Setup reality
Install with `npm install css-selector-parser`; there are no runtime dependencies, peer dependencies, native builds, credentials, or config files. The package publishes separate ESM and CommonJS entries plus TypeScript declarations, so both `import { createParser }` and `require('css-selector-parser')` resolve. The first real decision is grammar policy. `createParser()` defaults to the latest predefined syntax and strict parsing. Use `syntax: 'selectors-3'` when old compatibility matters, `syntax: 'progressive'` when unknown pseudo names should survive, or pass selected modules for features such as `&`, `:host`, and `::part()`. Set `strict: false` only when you intentionally want browser-like forgiveness for inputs such as an unclosed attribute selector. Substitutes such as `$variable` are nonstandard and remain disabled unless `substitutes: true` is set. Parse errors throw, so tools processing user input need a try/catch and should retain the original selector for diagnostics. Version 3 is not source compatible with earlier majors: the old configurable `CssSelectorParser` class is gone, tag, class, id, attribute, and pseudo data now live in `Rule.items`, nested rules use `combinator` and `nestedRule`, and `render` is a standalone export. Treat parsed nodes as a versioned AST contract, especially if serialized to disk or sent between services.
Patterns
Parse a modern selectorparse-selector
import { createParser } from 'css-selector-parser'
const parse = createParser()
const selector = parse('.card:has(> img) > a[href]')
console.log(selector.rules)createParser defaults to latest syntax and strict parsing; parsing invalid input throws.
Parse a comma-separated selector listparse-selector-list
const parse = createParser()
const ast = parse('h1, h2.title, article > p')
for (const rule of ast.rules) {
console.log(rule.items)
}Top-level Selector.rules represents comma-separated alternatives; combinator chains are linked through nestedRule.
Restrict parsing to Selectors Level 3choose-css-level
const parse = createParser({ syntax: 'selectors-3' })
const ast = parse('a[href^="https"]:first-child')Use an explicit level when unsupported newer syntax should be rejected rather than silently accepted.
Accept unknown pseudo namesaccept-progressive-syntax
const parse = createParser({ syntax: 'progressive' })
const ast = parse('button:vendor-state::vendor-part')Progressive mode adds acceptance for unknown pseudo-classes, pseudo-elements, and attribute case modifiers; it does not define their semantics.
Enable nesting and Shadow DOM modulesenable-css-modules
const parse = createParser({
syntax: 'selectors-4',
modules: ['css-nesting-1', 'css-scoping-1', 'css-shadow-parts-1'],
})
parse('& > :host .tab::part(label)')Module names are typed; enable only the specifications accepted by the tool consuming the AST.
Use browser-like forgiving parsingparse-forgivingly
const parse = createParser({ syntax: 'css3', strict: false })
const ast = parse('[data-state=open')strict defaults to true. Turning it off can accept incomplete input, which is useful for editors but risky for validation.
Collect class names by traversing the ASTcollect-class-names
import { createParser, traverse } from 'css-selector-parser'
const classes = new Set()
const root = createParser()('main.card > .title.active')
traverse(root, (node) => {
if (node.type === 'ClassName') classes.add(node.name)
})Traversal was added in version 3.3.0; older 3.x installations need upgrading before this export exists.
Inspect parents during traversalinspect-parent-context
traverse(root, (node, context) => {
console.log({
type: node.type,
parent: context.parent?.type,
depth: context.parents.length,
key: context.key,
index: context.index,
})
})index is present only for array-held children; parent is undefined for the root node.
Skip pseudo-class arguments while traversingskip-subtrees
traverse(root, (node) => {
if (node.type === 'PseudoClass') return false
inspect(node)
})Returning false skips that node's children, which matters for selector-valued arguments such as :has().
Build a selector with AST helpersbuild-selector
import { ast, render } from 'css-selector-parser'
const root = ast.selector({
rules: [ast.rule({
items: [
ast.tagName({ name: 'a' }),
ast.className({ name: 'button' }),
ast.attribute({ name: 'aria-current', operator: '=', value: ast.string({ value: 'page' }) }),
],
})],
})
console.log(render(root))Factory helpers produce the exact discriminated node shapes expected by render and TypeScript.
Render a parsed AST back to CSSrender-selector
import { createParser, render } from 'css-selector-parser'
const root = createParser()('div.notice > a[href]')
const css = render(root)
console.log(css)Rendering preserves selector meaning, not necessarily the source's original whitespace or escape spelling.
Parse nonstandard variable substitutesenable-substitutes
const parse = createParser({
syntax: 'progressive',
substitutes: true,
})
const ast = parse('[data-owner=$user]')Dollar substitutes are a package extension, not standard CSS; do not pass rendered output to browsers before replacing them.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| postcss-selector-parser | npm | Choose it for selector mutation inside an existing PostCSS plugin or stylesheet transform |
| css-what | npm | Choose it when building a selector matcher around css-select and its compact token format |
| parsel | npm | Choose it for a small browser-oriented selector parser with specificity-related utilities |