Warning
This is a very experimental CSS parser. Expect several bugs and inconveniences!
High-performance CSS parser built for static analysis and tooling
Parses large CSS files in microseconds with a fixed-size memory arena — no garbage collection pressure, no per-node allocations, no surprises. If you're building a formatter, linter, or analysis tool and need raw speed with precise source locations, this is built for that.
Heavily influenced by CSSTree. Some parsing mechanics are shared, but the memory model, architecture, and API are fundamentally different — not a fork.
- Full modern CSS — CSS Nesting,
:is(),:where(),:has(),@layer,@container - Error-tolerant — keeps parsing through malformed input; one bad rule won't kill the file
- Precise source locations — line, column, offset, and length on every node
- Tiny footprint — tree-shakeable subparsers; as small as 278 bytes gzipped, < 16 kB kB worst case
npm install @projectwallace/css-parserimport { parse, STYLE_RULE, DECLARATION } from '@projectwallace/css-parser'
const ast = parse(`
body {
color: red;
margin: 0;
}
@media (min-width: 768px) {
.container {
max-width: 1200px;
}
}
`)
// Iterate over top-level rules
for (const rule of ast) {
if (rule.type === STYLE_RULE) {
const selector = rule.first_child
console.log(`Selector: ${selector.text}`)
// Iterate over declarations
for (const node of rule) {
if (node.type === DECLARATION) {
console.log(` ${node.property}: ${node.value}`)
}
}
}
}- Zero allocations during parsing — memory is reserved upfront using real-world heuristics; the GC never runs mid-parse
- Cache-friendly layout — all nodes live in a single contiguous arena, making sequential access fast
- Location tracking with no penalty — full line, column, and offset data without slowing the parser down
- No syntax validation — skipping spec checks and MDN data means nothing gets in the way of speed
All sizes are minified and bundled with dependencies. Import only what you need — bundlers will tree-shake the rest.
| Import | Description | Minified | Gzip |
|---|---|---|---|
@projectwallace/css-parser |
Full parser (all subparsers) | 68.9 kB | 15.5 kB |
@projectwallace/css-parser/parse |
CSS stylesheet parser | 62.2 kB | 13.3 kB |
@projectwallace/css-parser/parse-atrule-prelude |
At-rule prelude parser | 35.4 kB | 8.4 kB |
@projectwallace/css-parser/parse-selector |
Selector parser | 37.7 kB | 8.9 kB |
@projectwallace/css-parser/parse-anplusb |
An+B syntax parser | 26.1 kB | 6.6 kB |
@projectwallace/css-parser/parse-declaration |
Declaration parser | 28.3 kB | 7.3 kB |
@projectwallace/css-parser/parse-value |
Value parser | 25.2 kB | 6.6 kB |
@projectwallace/css-parser/parse-dimension |
Dimension parser | 0.4 kB | 0.3 kB |
@projectwallace/css-parser/tokenizer |
Tokenizer | 9.8 kB | 2.4 kB |
Every function is exported from the package root and, where listed in Bundle sizes, from its own entry point. Node properties and return types ship as TypeScript types, so this section sticks to short examples.
Parses a full stylesheet and returns the root StyleSheet node. Nodes are iterable and expose first_child, next_sibling, children, text, line, column, start, end and length.
import { parse } from '@projectwallace/css-parser'
const ast = parse('body { color: red; }')
const rule = ast.first_child
rule.block.first_child.property // "color"
rule.block.first_child.value.text // "red"Options:
parse_values(defaulttrue) — parse declaration values into nodesparse_selectors(defaulttrue) — parse selectors into nodesparse_atrule_preludes(defaulttrue) — parse at-rule preludes into nodeson_comment— called for every comment; comments are never part of the tree
const comments = []
parse('/* hi */ a { color: red }', {
parse_values: false,
on_comment: (info) => comments.push(info), // { start, end, length, line, column }
})parse_selector returns the first selector, parse_selector_list the whole SelectorList.
import { parse_selector, parse_selector_list } from '@projectwallace/css-parser'
parse_selector('a.b > c').children.map((part) => part.text) // ["a", ".b", ">", "c"]
parse_selector_list('h1, h2').children.map((s) => s.text) // ["h1", "h2"]import { parse_declaration } from '@projectwallace/css-parser'
const decl = parse_declaration('color: red !important')
decl.property // "color"
decl.is_important // true
decl.value.text // "red"Parses a declaration value into a Value node with one child per component.
import { parse_value } from '@projectwallace/css-parser'
for (const node of parse_value('10px 20px')) {
console.log(node.type_name, node.value, node.unit) // "Dimension" 10 "px", then "Dimension" 20 "px"
}Returns an array of nodes for the prelude of the at-rule called name (without the @).
import { parse_atrule_prelude } from '@projectwallace/css-parser'
const [query] = parse_atrule_prelude('media', '(min-width: 768px)')
query.type_name // "MediaQuery"Splits a number from its unit. It works for anything shaped like a number followed by a unit: lengths (100px), percentages (2%), durations (200ms) and so on. It does not validate; text without a leading number gives value: 0.
import { parse_dimension } from '@projectwallace/css-parser'
parse_dimension('100px') // { value: 100, unit: "px" }
parse_dimension('2%') // { value: 2, unit: "%" }
parse_dimension('1.5e3s') // { value: 1500, unit: "s" }
parse_dimension('5') // { value: 5, unit: "" }Both walk the tree depth-first. Return SKIP to skip the children of a node and BREAK to stop entirely. walk also passes a depth that counts rule and at-rule nesting. traverse calls enter before and leave after the children (leave still runs after SKIP, but not after BREAK).
import {
parse,
walk,
traverse,
SKIP,
BREAK,
DECLARATION,
AT_RULE,
} from '@projectwallace/css-parser'
const ast = parse('@media screen { a { color: red; margin: 0 } }')
walk(ast, (node) => {
if (node.type === DECLARATION) {
console.log(node.property) // "color"
return BREAK
}
})
traverse(ast, {
enter(node) {
if (node.type === AT_RULE) return SKIP // leave() still runs for this node
},
leave(node) {},
})A generator of tokens; comments are skipped and reported through on_comment.
import { tokenize } from '@projectwallace/css-parser'
for (const token of tokenize('a { color: red }')) {
console.log(token.type, token.start, token.end)
}Node type constants (STYLE_RULE, DECLARATION, DIMENSION, …), TYPE_NAMES and NODE_TYPES are exported from the root, as are type guards such as is_declaration() and is_dimension() that narrow a node to its specific type. node.clone({ deep?, locations? }) returns a mutable plain-object copy of a node.
import { parse, is_declaration, TYPE_NAMES, DECLARATION } from '@projectwallace/css-parser'
const decl = parse('a { color: red }').first_child.block.first_child
is_declaration(decl) // true
TYPE_NAMES[DECLARATION] // "Declaration"
decl.clone({ deep: false }).property // "color"Allocation-free helpers, handy when working with node names. The lowercase argument is the first one for str_equals and the second one for str_starts_with and str_index_of. This inconsistency will be fixed in a future breaking change.
import {
is_custom,
is_vendor_prefixed,
str_equals,
str_starts_with,
str_index_of,
} from '@projectwallace/css-parser'
is_custom('--primary') // true
is_vendor_prefixed('-webkit-transform') // true
str_equals('media', 'MEDIA') // true
str_starts_with('WEBKIT-transform', 'webkit') // true
str_index_of('background-COLOR', 'color') // 11- No syntax validation — CSS structure is your responsibility; the parser accepts anything valid or not
- No preprocessor syntax — Sass, Less, and Stylus are out of scope; this parser targets plain CSS only
- Tree operations - Manipulating the AST is not within scope of this library
MIT