Package detail

css-selector-parser

mdevils5.8mMIT3.1.3

Powerful and compliant CSS selector parser.

css, css selector, css selector parser, pseudo-classes

readme

css-selector-parser

npm npm bundle size NPM License GitHub stars

A high-performance CSS selector parser with advanced features for modern web development.

Features

  • 🚀 Fast and memory-efficient parsing for all CSS selectors
  • 🌳 AST-based object model for programmatic manipulation
  • 📊 Full compliance with all CSS selector specifications
  • 🧪 Comprehensive test coverage
  • 📚 Well-documented API with TypeScript support
  • 🔄 Two-way conversion between CSS selectors and AST
  • 🧩 Modular support for various CSS specifications

Supported CSS Selector Standards

Migration Guides

See Changelog for release details.

Installation

npm install css-selector-parser
# or
yarn add css-selector-parser
# or
pnpm add css-selector-parser

Usage

Parsing Selectors

import { createParser } from 'css-selector-parser';

const parse = createParser();
const selector = parse('a[href^="/"], .container:has(nav) > a[href]:nth-child(2)::before');

console.log(selector);

This produces an AST (Abstract Syntax Tree) output:

({
    type: 'Selector',
    rules: [
        {
            type: 'Rule',
            items: [
                { type: 'TagName', name: 'a' },
                {
                    type: 'Attribute',
                    name: 'href',
                    operator: '^=',
                    value: { type: 'String', value: '/' }
                }
            ]
        },
        {
            type: 'Rule',
            items: [
                { type: 'ClassName', name: 'container' },
                {
                    type: 'PseudoClass',
                    name: 'has',
                    argument: {
                        type: 'Selector',
                        rules: [
                            {
                                type: 'Rule',
                                items: [ { type: 'TagName', name: 'nav' } ]
                            }
                        ]
                    }
                }
            ],
            nestedRule: {
                type: 'Rule',
                items: [
                    { type: 'TagName', name: 'a' },
                    { type: 'Attribute', name: 'href' },
                    {
                        type: 'PseudoClass',
                        name: 'nth-child',
                        argument: { type: 'Formula', a: 0, b: 2 }
                    },
                    {
                        type: 'PseudoElement',
                        name: 'before'
                    }
                ],
                combinator: '>'
            }
        }
    ]
})

Building and Rendering Selectors

import { ast, render } from 'css-selector-parser';

const selector = ast.selector({
    rules: [
        ast.rule({
            items: [
                ast.tagName({name: 'a'}),
                ast.attribute({name: 'href', operator: '^=', value: ast.string({value: '/'})})
            ]
        }),
        ast.rule({
            items: [
                ast.className({name: 'container'}),
                ast.pseudoClass({
                    name: 'has',
                    argument: ast.selector({
                        rules: [ast.rule({items: [ast.tagName({name: 'nav'})]})]
                    })
                })
            ],
            nestedRule: ast.rule({
                combinator: '>',
                items: [
                    ast.tagName({name: 'a'}),
                    ast.attribute({name: 'href'}),
                    ast.pseudoClass({
                        name: 'nth-child',
                        argument: ast.formula({a: 0, b: 2})
                    }),
                    ast.pseudoElement({name: 'before'})
                ]
            })
        })
    ]
});

console.log(render(selector)); // a[href^="/"], .container:has(nav) > a[href]:nth-child(2)::before

CSS Modules Support

CSS Modules are specifications that add new selectors or modify existing ones. This parser supports various CSS modules that can be included in your syntax definition:

import { createParser } from 'css-selector-parser';

// Create a parser with specific CSS modules enabled
const parse = createParser({
    syntax: 'selectors-4',
    modules: ['css-position-3', 'css-scoping-1']
});

Supported CSS Modules

Module Description
css-position-1/2/3/4 Position-related pseudo-classes
css-scoping-1 Shadow DOM selectors (:host, :host-context(), ::slotted())
css-pseudo-4 Modern pseudo-elements (::selection, ::backdrop, etc.)
css-shadow-parts-1 ::part() for styling shadow DOM components

The latest syntax automatically includes all modules marked as current specifications.

API Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Security Contact

To report a security vulnerability, please use the Tidelift security contact. Tidelift will coordinate the fix and disclosure.

Sponsorship

If you find this project useful, please consider sponsoring the developer or supporting on Patreon.

changelog

Changelog

All notable changes to this project will be documented in this file. See standard-version for commit guidelines.

3.1.3 (2025-06-23)

Bug Fixes

  • ::part() takes a string, not a selector (db4dd40)

3.1.2 (2025-04-13)

Bug Fixes

3.1.1 (2025-03-16)

3.1.0 (2025-03-16)

Features

  • Add "css-pseudo-4" CSS Module with new pseudo-classes and pseudo-elements (16c1bb1)
  • Add "modules" property to SyntaxDefinition and include latest CSS modules (cd56683)
  • Add comprehensive tests for CSS features option (74137f0)
  • Add CSS modules tests for position, scoping, and multiple module support (4cd3ecd)
  • Add css-position-1 and css-position-2 modules with tests (59b1161)
  • Add location information to pseudo-class and pseudo-element error messages (bafafe9)
  • Add Selectors Level 4 experimental features and syntax definitions (9cda2ca)
  • Add support for CSS feature modules in parser configuration (42ba90e)
  • Add support for CSS Shadow Parts module with ::part() pseudo-element (6dbe132)
  • in case of undefined pseudos, show in which css level / module it is defined (894053a)
  • latest should include all latest modules (6d7a116)

Bug Fixes

3.0.5 (2024-03-02)

Bug Fixes

  • single hyphen is not a valid identifier, throwing an exception, closes #38 (2520a49)

3.0.4 (2023-12-15)

Bug Fixes

  • align identifier and string parsing and rendering with CSS standards, closes #36, closes #37 (ac0dbc0)

3.0.3 (2023-12-08)

Bug Fixes

  • align identifier parsing with CSS standards and browser behaviour (6087705)

3.0.2 (2023-11-21)

Bug Fixes

  • identifier parsing for ids, classes, pseudo-classes and pseudo-elements (d222dfd)

3.0.1 (2023-11-20)

Bug Fixes

3.0.0 (2023-09-26)

⚠ BREAKING CHANGES

  • API is backwards incompatible.

Migrating from 2.x to 3.x

  1. Rule.tag was moved to Rule.items.

    Example selector: div.

    • Before: {type: 'Rule', tagName: {type: 'TagName', name: 'div'}}
    • After: {type: 'Rule', items: [{type: 'TagName', name: 'div'}]}
  2. Rule.classNames was converted to an AST entity and moved to Rule.items.

    Example selector: .user.hidden

    • Before: {type: 'Rule', classNames: ['user', 'hidden']}
    • After: {type: 'Rule', items: [{type: 'ClassName', name: 'user'}, {type: 'ClassName', name: 'hidden'}]}
  3. Rule.ids was converted to an AST entity and moved to Rule.items.

    Example selector: #root#user-1

    • Before: {type: 'Rule', ids: ['root', 'user-1']}
    • After: {type: 'Rule', items: [{type: 'Id', name: 'root'}, {type: 'Id', name: 'user-1'}]}
  4. Rule.attributes was moved to Rule.items.

    Example selector: [href^=/][role]

    • Before: {type: 'Rule', attributes: [{type: 'Attribute', name: 'href', operator: '^=', value: {type: 'String', value: '/'}}, {type: 'Attribute', name: 'role'}]}
    • After: {type: 'Rule', items: [{type: 'Attribute', name: 'href', operator: '^=', value: {type: 'String', value: '/'}}, {type: 'Attribute', name: 'role'}]}
  5. Rule.pseudoClasses was moved to Rule.items.

    Example selector: :hover:lang(en)

    • Before: {type: 'Rule', pseudoClasses: [{type: 'PseudoClass', name: 'hover'}, {type: 'PseudoClass', name: 'lang', value: {type: 'String', value: 'en'}}]}
    • After: {type: 'Rule', items: [{type: 'PseudoClass', name: 'hover'}, {type: 'PseudoClass', name: 'lang', value: {type: 'String', value: 'en'}}]}
  6. Rule.pseudoElement was converted to an AST entity and moved to Rule.items.

    Example selector: ::before

    • Before: {type: 'Rule', pseudoElement: 'before'}
    • After: {type: 'Rule', items: [{type: 'PseudoElement', name: 'before'}]}

New AST methods

  • ast.id and ast.isId to create and test ast nodes with type Id.
  • ast.className and ast.isClassName to create and test ast nodes with type ClassName.
  • ast.pseudoElement and ast.isPseudoElement to create and test ast nodes with type PseudoElement.

New Syntax Definition configuration

  • pseudoElements.definitions was updated to accept signatures in otder to support specifying pseudo-elements with an argument. Example: createParser({syntax: {pseudoElements: {definitions: {NoArgument: ['before'], String: ['highlight'], Selector: ['slotted']}}}}).

Migrating from 1.x to 3.x

CssSelectorParser -> createParser

In 1.x versions there was CssSelectorParser class which had to be contructed and then configured. In 3.x versions there is createParser() function which returns a parse() function. All the configutation is passed to createParser() params.

Before:

var CssSelectorParser = require('css-selector-parser').CssSelectorParser,
parser = new CssSelectorParser();
parser.registerSelectorPseudos('has');
parser.registerNumericPseudos('nth-child');
parser.registerNestingOperators('>', '+', '~');
parser.registerAttrEqualityMods('^', '$', '*', '~');

const selector = parser.parse('a[href^=/], .container:has(nav) > a[href]:lt($var):nth-child(5)');

After:

import {createParser} from 'css-selector-parser';

const parse = createParser({
    syntax: {
        pseudoClasses: {
            // In 1.x any pseudo-classes were accepted.
            // in 2.x parser only accepts known psuedo-classes unless `unknown: accept` was specified. 
            unknown: 'accept',
            definitions: {
                // This is a replacement for registerSelectorPseudos().
                Selector: ['has'],
                // This is a replacement for registerNumericPseudos().
                Formula: ['nth-child']
            }
        },
        // This is a replacement for registerNestingOperators().
        combinators: ['>', '+', '~'],
        attributes: {
            // This a replacement for registerAttrEqualityMods().
            // Note that equals sign ("=") is included into the operator definitions.
            operators: ['^=', '$=', '*=', '~=']
        }
    },
    // This is a replacement for enableSubstitutes()
    substitutes: true
});

const selector = parse('a[href^=/], .container:has(nav) > a[href]:lt($var):nth-child(5)');

Predefined CSS syntax definitions

You no longer need to make an extensive configuration of css-selector-parser in order to make it understand the necessary CSS standards. You can now just define CSS/CSS selectors version directly:

import {createParser} from 'css-selector-parser';

const parse = createParser({syntax: 'css3'});

const selector = parse('a[href^=/], .container:has(nav) > a[href]:nth-child(2n + 1)::before');

Here are the pre-defined CSS standards for your disposal:

Make sure you use proper strict value

CSS selector parser in modern browsers is very forgiving. For instance, it works fine with unclosed attribute selectors: "[attr=value". If you would like to mimic this behavior from browsers, set strict to false, i.e.:

import {createParser} from 'css-selector-parser';

const parse = createParser({syntax: 'css3', strict: false});

const selector = parse(':lang(en'); // doesn't crash

Render is now a separate export

render() method used to be a method of CssSelectorParser class. Now it can be imported directly and used.

Example:

import {createParser, render} from 'css-selector-parser';

const parse = createParser({syntax: 'progressive'});

const selector = parse('div#user-123.user:lang(en)::before');

console.log(render(selector)); // div#user-123.user:lang(en)::before

AST changes

AST had a lot of changes.

Selector

New type info.

  1. Type changed: selector -> Selector.
  2. Prop changed: selectors -> rules, also selectors contained ruleSet[], which in turn has rule field, and new rules contains Rule[] directly.

Before: {type: 'selector', selectors: [ {type: 'ruleSet', rule: {<RULE 1 DATA>}}, {type: 'ruleSet', rule: {<RULE 2 DATA>}} ]}.

After: {type: 'Selector', rules: [ {<RULE 1 DATA>}, {<RULE 2 DATA>} ]}.

Rule

New type info.

  1. Type changed: rule -> Rule.
  2. Prop changed: id: string -> items: [{type: 'Id', name: '<ID>'}, ...]. According to the CSS spec one rule may have more than 1 id, so #root#root is a valid selector.
  3. Prop renamed: nestingOperator -> combinator. A proper name according to CSS spec was chosen.
  4. Prop renamed: rule -> nestedRule. A proper name to indicate nesting was chosen.
  5. Prop changed: tagName: string -> items: [TagName | WildcardTag, ...]. Using explicit distinction between TagName (i.e. div) and WildcardTag (*), because tag name can also be * if escaped properly (\*).
  6. Prop changed: attrs -> items: [<ATTRIBUTE>, ...]. Attribute type was changed, see below.
  7. Prop changed: pseudos -> items: [<PSEUDO CLASS>, ...]. There are pseudo-elements and pseudo-classes, now they are separated properly (there is a separate pseudoElement type). Pseudo class type was changed, see below.

Before:

({
    type: 'rule',
    tagName: 'div',
    id: 'user-123',
    classNames: ['user'],
    attrs: [
        {name: 'role', operator: '$=', valueType: 'string', value: 'button'}
    ],
    pseudos: [
        {name: 'lang', valueType: 'string', value: 'en'}
    ],
    nestingOperator: '>'
})

After:

({
    type: 'Rule',
    items: [
       {type: 'TagName', name: 'div'},
       {type: 'Id', name: 'user-123'},
       {type: 'ClassName', name: 'user'},
       {type: 'Attribute', name: 'role', operator: '$=', value: {type: 'String', value: 'button'}},
       {type: 'PseudoClass', name: 'lang', value: {type: 'String', value: 'en'}}
    ],
    combinator: '>'
})

Attribute

New type info.

  1. Type introduced: Attribute.
  2. Prop value and valueType were combined to a single prop value with a field type.

All possible value types.

Example 1

Before: {name: 'role'}.

After: {type: 'Attribute', name: 'role'}.

Example 2

Before: {name: 'role', operator: '$=', valueType: 'string', value: 'button'}.

After: {type: 'Attribute', name: 'role', operator: '$=', value: {type: 'String', value: 'button'}}.

Example 3

Before: {name: 'role', operator: '=', valueType: 'substitute', value: 'var'}.

After: {type: 'Attribute', name: 'role', operator: '=', value: {type: 'Substitute', name: 'var'}}.

Pseudo Classes

New type info.

  1. Type introduced: PseudoClass.
  2. Prop value and valueType were combined to a single prop argument with a field type.

All possible argument types.

Example 1

Before: {name: 'visited'}.

After: {type: 'PseudoClass', name: 'visited'}.

Example 2

Before: {name: 'lang', valueType: 'string', value: 'en'}.

After: {type: 'PseudoClass', name: 'lang', argument: {type: 'String', value: 'en'}}.

Example 3

Before: {name: 'lang', valueType: 'substitute', value: 'var'}.

After: {type: 'PseudoClass', name: 'lang', argument: {type: 'Substitute', name: 'var'}}.

Example 4

Before: {name: 'has', valueType: 'selector', value: {type: 'selector', selectors: [{type: 'ruleSet', rule: {type: 'rule', tagName: 'div'}}]}}.

After: {type: 'PseudoClass', name: 'has', argument: {type: 'Selector', rules: [{type: 'Rule', tag: {type: 'TagName', name: 'div'}}]}}.

Pseudo Elements

New type info.

  1. Type introduced: PseudoElement.

All possible argument types.

Features

  • upgrade API in order to reflect upcoming complexity in CSS selectors (cece4df)

2.3.2 (2023-06-25)

Bug Fixes

  • fix foruma parsing with negative A, closes #28 (824312f)
  • include js file extension into the mjs build, closes #22 (f50b350)
  • rendering nested selectors with combinators, closes #27 (40fb434)

2.3.1 (2023-06-24)

Bug Fixes

2.3.0 (2023-06-24)

Features

  • publish hybrid package: CommonJS and ESM modules (16fd8a1)

2.2.1-2.2.3

  • Update published docs.

2.2.0

  • Full refactoring.
  • Switch to typescript.
  • Pre-defined CSS syntaxes were included.
  • The whole AST was documented.

Migrating from 1.x

CssSelectorParser -> createParser

In 1.x versions there was CssSelectorParser class which had to be contructed and then configured. In 2.x versions there is createParser() function which returns a parse() function. All the configutation is passed to createParser() params.

Before:

var CssSelectorParser = require('css-selector-parser').CssSelectorParser,
parser = new CssSelectorParser();
parser.registerSelectorPseudos('has');
parser.registerNumericPseudos('nth-child');
parser.registerNestingOperators('>', '+', '~');
parser.registerAttrEqualityMods('^', '$', '*', '~');

const selector = parser.parse('a[href^=/], .container:has(nav) > a[href]:lt($var):nth-child(5)');

After:

import {createParser} from 'css-selector-parser';

const parse = createParser({
    syntax: {
        pseudoClasses: {
            // In 1.x any pseudo-classes were accepted.
            // in 2.x parser only accepts known psuedo-classes unless `unknown: accept` was specified. 
            unknown: 'accept',
            definitions: {
                // This is a replacement for registerSelectorPseudos().
                Selector: ['has'],
                // This is a replacement for registerNumericPseudos().
                Formula: ['nth-child']
            }
        },
        // This is a replacement for registerNestingOperators().
        combinators: ['>', '+', '~'],
        attributes: {
            // This a replacement for registerAttrEqualityMods().
            // Note that equals sign ("=") is included into the operator definitions.
            operators: ['^=', '$=', '*=', '~=']
        }
    },
    // This is a replacement for enableSubstitutes()
    substitutes: true
});

const selector = parse('a[href^=/], .container:has(nav) > a[href]:lt($var):nth-child(5)');

Predefined CSS syntax definitions

You no longer need to make an extensive configuration of css-selector-parser in order to make it understand the necessary CSS standards. You can now just define CSS/CSS selectors version directly:

import {createParser} from 'css-selector-parser';

const parse = createParser({syntax: 'css3'});

const selector = parse('a[href^=/], .container:has(nav) > a[href]:nth-child(2n + 1)::before');

Here are the pre-defined CSS standards for your disposal:

Make sure you use proper strict value

CSS selector parser in modern browsers is very forgiving. For instance, it works fine with unclosed attribute selectors: "[attr=value". If you would like to mimic this behavior from browsers, set strict to false, i.e.:

import {createParser} from 'css-selector-parser';

const parse = createParser({syntax: 'css3', strict: false});

const selector = parse(':lang(en'); // doesn't crash

Render is now a separate export

render() method used to be a method of CssSelectorParser class. Now it can be imported directly and used.

Example:

import {createParser, render} from 'css-selector-parser';

const parse = createParser({syntax: 'progressive'});

const selector = parse('div#user-123.user:lang(en)');

console.log(render(selector)); // div#user-123.user:lang(en)

AST changes

AST had a lot of changes.

Selector

New type info.

  1. Type changed: selector -> Selector.
  2. Prop changed: selectors -> rules, also selectors contained ruleSet[], which in turn has rule field, and new rules contains Rule[] directly.

Before: {type: 'selector', selectors: [ {type: 'ruleSet', rule: {<RULE 1 DATA>}}, {type: 'ruleSet', rule: {<RULE 2 DATA>}} ]}.

After: {type: 'Selector', rules: [ {<RULE 1 DATA>}, {<RULE 2 DATA>} ]}.

Rule

New type info.

  1. Type changed: rule -> Rule.
  2. Prop changed: id: string -> ids: string[]. According to the CSS spec one rule may have more than 1 id, so #root#root is a valid selector.
  3. Prop renamed: nestingOperator -> combinator. A proper name according to CSS spec was chosen.
  4. Prop renamed: rule -> nestedRule. A proper name to indicate nesting was chosen.
  5. Prop changed: tagName: string -> tag: TagName | WildcardTag. Using explicit distinction between TagName (i.e. div) and WildcardTag (*), because tag name can also be * if escaped properly (\*).
  6. Prop changed: attrs -> attributes. Attribute type was changed, see below.
  7. Prop changed: pseudos -> pseudoClasses. There are pseudo-elements and pseudo-classes, now they are separated properly (there is a separate pseudoElement property). Pseudo class type was changed, see below.

Before:

({
    type: 'rule',
    tagName: 'div',
    id: 'user-123',
    classNames: ['user'],
    attrs: [
        {name: 'role', operator: '$=', valueType: 'string', value: 'button'}
    ],
    pseudos: [
        {name: 'lang', valueType: 'string', value: 'en'}
    ],
    nestingOperator: '>'
})

After:

({
    type: 'Rule',
    tag: {type: 'TagName', name: 'div'},
    ids: ['user-123'],
    classNames: ['user'],
    attributes: [
        {type: 'Attribute', name: 'role', operator: '$=', value: {type: 'String', value: 'button'}}
    ],
    pseudoClasses: [
        {type: 'PseudoClass', name: 'lang', value: {type: 'String', value: 'en'}}
    ],
    combinator: '>'
})

Attribute

New type info.

  1. Type introduced: Attribute.
  2. Prop value and valueType were combined to a single prop value with a field type.

All possible value types.

Example 1

Before: {name: 'role'}.

After: {type: 'Attribute', name: 'role'}.

Example 2

Before: {name: 'role', operator: '$=', valueType: 'string', value: 'button'}.

After: {type: 'Attribute', name: 'role', operator: '$=', value: {type: 'String', value: 'button'}}.

Example 3

Before: {name: 'role', operator: '=', valueType: 'substitute', value: 'var'}.

After: {type: 'Attribute', name: 'role', operator: '=', value: {type: 'Substitute', name: 'var'}}.

Pseudo Classes

New type info.

  1. Type introduced: PseudoClass.
  2. Prop value and valueType were combined to a single prop argument with a field type.

All possible argument types.

Example 1

Before: {name: 'visited'}.

After: {type: 'PseudoClass', name: 'visited'}.

Example 2

Before: {name: 'lang', valueType: 'string', value: 'en'}.

After: {type: 'PseudoClass', name: 'lang', argument: {type: 'String', value: 'en'}}.

Example 3

Before: {name: 'lang', valueType: 'substitute', value: 'var'}.

After: {type: 'PseudoClass', name: 'lang', argument: {type: 'Substitute', name: 'var'}}.

Example 4

Before: {name: 'has', valueType: 'selector', value: {type: 'selector', selectors: [{type: 'ruleSet', rule: {type: 'rule', tagName: 'div'}}]}}.

After: {type: 'PseudoClass', name: 'has', argument: {type: 'Selector', rules: [{type: 'Rule', tag: {type: 'TagName', name: 'div'}}]}}.