A fast and lightweight CSS selector parser that transforms CSS selector strings into structured AST objects
npx @tessl/cli install tessl/npm-css-what@7.0.0CSS What is a fast and lightweight CSS selector parser that transforms CSS selector strings into structured AST (Abstract Syntax Tree) objects. It supports parsing all standard CSS selector types including tags, classes, IDs, attributes, pseudo-classes, pseudo-elements, and combinators, with comprehensive support for CSS3 selectors and modern selector features.
npm install css-whatimport { parse, isTraversal, stringify, type Selector, SelectorType, AttributeAction } from "css-what";For CommonJS:
const { parse, isTraversal, stringify, SelectorType } = require("css-what");import { parse, stringify, isTraversal } from "css-what";
// Parse a CSS selector into an AST
const ast = parse("div.container > p:nth-child(2n+1)[data-id]");
// Convert AST back to string
const selectorString = stringify(ast);
// Check if a selector token is a traversal (combinator)
ast[0].forEach(selector => {
if (isTraversal(selector)) {
console.log(`Found combinator: ${selector.type}`);
}
});Parses a CSS selector string into a two-dimensional array of selector tokens.
/**
* Parses a CSS selector string into a structured AST
* @param selector - CSS selector string to parse
* @returns Two-dimensional array where first dimension represents comma-separated selectors, second contains tokens for each selector
* @throws Error for invalid selectors
*/
function parse(selector: string): Selector[][];The returned structure represents comma-separated selectors as the first dimension, with each selector broken down into individual tokens (tags, classes, IDs, attributes, pseudo-classes, combinators, etc.).
Important: Class selectors (.class) are represented as attribute selectors with name: "class" and action: "element". ID selectors (#id) are represented as attribute selectors with name: "id" and action: "equals".
Usage Examples:
// Simple selector
const simple = parse("div");
// Result: [[{ type: "tag", name: "div", namespace: null }]]
// Complex selector with multiple parts
const complex = parse("div.container#main[data-role='content']:hover");
// Result: [[
// { type: "tag", name: "div", namespace: null },
// { type: "attribute", name: "class", action: "element", value: "container", ignoreCase: "quirks", namespace: null },
// { type: "attribute", name: "id", action: "equals", value: "main", ignoreCase: "quirks", namespace: null },
// { type: "attribute", name: "data-role", action: "equals", value: "content", ignoreCase: null, namespace: null },
// { type: "pseudo", name: "hover", data: null }
// ]]
// Multiple selectors (comma-separated)
const multiple = parse("div, span.highlight");
// Result: [
// [{ type: "tag", name: "div", namespace: null }],
// [{ type: "tag", name: "span", namespace: null }, { type: "attribute", name: "class", action: "element", value: "highlight", ignoreCase: "quirks", namespace: null }]
// ]
// Selector with combinators
const withCombinators = parse("div > p + span");
// Result: [[
// { type: "tag", name: "div", namespace: null },
// { type: "child" },
// { type: "tag", name: "p", namespace: null },
// { type: "adjacent" },
// { type: "tag", name: "span", namespace: null }
// ]]Converts a parsed selector array back into a CSS selector string.
/**
* Converts a parsed selector array back into a CSS selector string
* @param selector - Two-dimensional array of selector tokens from parse()
* @returns CSS selector string
*/
function stringify(selector: Selector[][]): string;Usage Examples:
// Round-trip parsing and stringification
const original = "div.container > p:nth-child(2)";
const ast = parse(original);
const reconstructed = stringify(ast);
// reconstructed === original
// Modify AST and stringify
const ast = parse("div");
ast[0].push({ type: SelectorType.Attribute, name: "class", action: AttributeAction.Element, value: "highlight", ignoreCase: "quirks", namespace: null });
const result = stringify(ast);
// result === "div.highlight"Type guard function that checks whether a selector token is a traversal (combinator).
/**
* Type guard that checks whether a selector is a traversal (combinator)
* @param selector - Selector object to check
* @returns Boolean indicating if selector is a traversal type
*/
function isTraversal(selector: Selector): selector is Traversal;Usage Examples:
const ast = parse("div > p");
ast[0].forEach(selector => {
if (isTraversal(selector)) {
console.log(`Found combinator: ${selector.type}`); // "child"
} else {
console.log(`Found selector: ${selector.type}, name: ${selector.name}`);
}
});/**
* Union type representing any CSS selector token
*/
type Selector = PseudoSelector | PseudoElement | AttributeSelector | TagSelector | UniversalSelector | Traversal;
/**
* Data type for pseudo-selector content
*/
type DataType = Selector[][] | null | string;
/**
* Union type for traversal (combinator) selector types
*/
type TraversalType = SelectorType.Adjacent | SelectorType.Child | SelectorType.Descendant | SelectorType.Parent | SelectorType.Sibling | SelectorType.ColumnCombinator;enum SelectorType {
// Basic selectors
Attribute = "attribute",
Pseudo = "pseudo",
PseudoElement = "pseudo-element",
Tag = "tag",
Universal = "universal",
// Combinators (traversals)
Adjacent = "adjacent", // +
Child = "child", // >
Descendant = "descendant", // (whitespace)
Parent = "parent", // < (non-standard)
Sibling = "sibling", // ~
ColumnCombinator = "column-combinator" // ||
}/**
* Represents an attribute selector like [attr=value]
*/
interface AttributeSelector {
type: SelectorType.Attribute;
name: string; // Attribute name
action: AttributeAction; // Comparison operation
value: string; // Attribute value
ignoreCase: "quirks" | boolean | null; // Case sensitivity flag
namespace: string | null; // XML namespace
}
/**
* Represents a pseudo-class selector like :hover, :nth-child(2n+1)
*/
interface PseudoSelector {
type: SelectorType.Pseudo;
name: string; // Pseudo-class name
data: DataType; // Associated data (null, string, or nested selectors)
}
/**
* Represents a pseudo-element selector like ::before, ::after
*/
interface PseudoElement {
type: SelectorType.PseudoElement;
name: string; // Pseudo-element name
data: string | null; // Associated data
}
/**
* Represents a tag selector like div, span, h1
*/
interface TagSelector {
type: SelectorType.Tag;
name: string; // Tag name
namespace: string | null; // XML namespace
}
/**
* Represents the universal selector *
*/
interface UniversalSelector {
type: SelectorType.Universal;
namespace: string | null; // XML namespace
}
/**
* Represents a combinator like >, +, ~, (space)
*/
interface Traversal {
type: TraversalType; // Type of combinator
}/**
* Enumeration of attribute selector operations
*/
enum AttributeAction {
Any = "any", // *= (contains substring)
Element = "element", // ~= (contains word)
End = "end", // $= (ends with)
Equals = "equals", // = (exact match)
Exists = "exists", // [attr] (attribute exists)
Hyphen = "hyphen", // |= (starts with, hyphen-separated)
Not = "not", // != (not equal, non-standard)
Start = "start" // ^= (starts with)
}/**
* Standard values for case sensitivity modes in attribute selectors
* Used to specify how attribute value matching should handle case sensitivity
*/
const IgnoreCaseMode = {
Unknown: null, // Case sensitivity is unknown/unspecified
QuirksMode: "quirks", // Browser quirks mode behavior (default for class/id)
IgnoreCase: true, // Force case-insensitive matching
CaseSensitive: false // Force case-sensitive matching
} as const;div, span, h1*.class-name#element-id[attr], [attr=value], [attr^=value], [attr$=value], [attr*=value], [attr~=value], [attr|=value]:hover, :focus, :nth-child(), :not(), :has(), etc.::before, ::after, ::first-line, etc. (whitespace)>+~< (non-standard, used by some libraries)|| (CSS Grid)\ escaped characters in names, attributes, and valuesnamespace|element, supports wildcard *|element and empty namespace |element[attr=value i] (case-insensitive), [attr=value s] (case-sensitive):has(), :not(), :matches(), :is(), :where(), :host(), :host-context()/* */ comments within selectorsThe parser throws errors for various invalid selector conditions:
// Unmatched selector parts
parse("div ["); // Error: Attribute selector didn't terminate
// Invalid parentheses
parse("div:nth-child(2"); // Error: Parenthesis not matched
// Empty selectors
parse(", div"); // Error: Empty sub-selector
// Invalid characters in specific contexts
parse("div:has('invalid')"); // Error: Pseudo-selector has cannot be quotedFull XML namespace support for elements and attributes:
// Namespace prefix
const nsElement = parse("svg|rect");
// Result: [{ type: "tag", name: "rect", namespace: "svg" }]
// Universal namespace
const anyNs = parse("*|div");
// Result: [{ type: "tag", name: "div", namespace: "*" }]
// Empty namespace (no namespace)
const noNs = parse("|div");
// Result: [{ type: "tag", name: "div", namespace: "" }]
// Namespaced attributes
const nsAttr = parse("[xml|lang='en']");
// Result: [{ type: "attribute", name: "lang", namespace: "xml", action: "equals", value: "en", ignoreCase: null }]Special handling for pseudo-classes that contain nested selectors:
// Selector-containing pseudo-classes parse nested selectors
const has = parse("div:has(> p.highlight)");
// data property contains parsed Selector[][] structure
// String-containing pseudo-classes store raw string data
const nthChild = parse("li:nth-child(2n+1)");
// data property contains "2n+1" as string
// Quote stripping for specific pseudo-classes
const contains = parse("div:contains('text content')");
// Quotes automatically stripped from :contains and :icontains