A zero-allocation, high-performance CSS processing engine for Node.js and the browser, combining parsing, validation, transformation, minification, and AST-based tooling in a single solution.
Every stylesheet is fully parsed into a structured AST, with token values represented as typed data. This provides a reliable semantic foundation for custom transformations, plugin development, and advanced stylesheet analysis without relying on raw string manipulation.
Note
Version 2.0 has been rewritten to use arena memory for AST nodes and tokens, improving memory usage and performance. On the Tailwind CSS parsing and validation workload, runtime is 42% faster than v1.8.2 while using 60% less memory.
This major release also introduces breaking changes and removes deprecated features. See the migration guide for details.
From npm
$ npm install @tbela99/css-parserfrom jsr
$ deno add @tbela99/css-parser- Zero dependencies — lightweight and easy to integrate into any project.
- All-in-one CSS parsing solution covering parsing, validation, transformation, minification, and AST manipulation.
- Standards-based CSS validation powered by MDN data.
- Full CSS Modules support for modern component-based workflows.
- Fault-tolerant parsing that follows the CSS Syntax Module Level 3 specification and always produces a complete, structured parse result.
- Typed tokens and AST — parsed CSS is exposed as strongly typed tokens and nodes so plugins and transforms can operate on semantic structures.
- High-performance minification with safe optimizations and no unsafe transforms.
- Advanced color processing with support for modern color spaces and functions, including
color(),lab(),lch(),oklab(),oklch(),color-mix(),light-dark(), system colors, and relative colors. - Color conversion engine capable of transforming colors between all supported formats.
- Automatic CSS nesting generation from compatible selectors.
- Source map generation for easier debugging and development workflows.
- Shorthand property computation to reduce output size and improve optimization.
- Transform function optimization for more compact CSS output.
- Math function evaluation, including
calc(),clamp(),min(),max(), and related expressions. - CSS variable inlining where values can be safely resolved.
- Duplicate declaration removal to eliminate redundant rules.
@importflattening to produce self-contained stylesheets.- 51.5kb once minified and gzipped.
Vendor prefix cleanup to modernize generated CSS.
CSS-Parser can transform these modern CSS features into lower-level CSS syntax:
- Nested CSS transpilation to legacy-compatible syntax.
if()function transpilation for broader browser compatibility.color-mix()function conversion convert new color-mix() syntax to any of the supported colors.
Across all tested datasets, css-parser consistently produces the smallest output among the benchmarked tools, including Lightning CSS, cssnano, csso, clean-css, css-tree, and esbuild.
The benchmark evaluates minification effectiveness on a diverse set of real-world stylesheets, including Bootstrap 4, Bootstrap 5, Tailwind CSS, Animate.css, Foundation, Font Awesome, Normalize.css, and others.
A sample result:
| File | ligthningcss | CSS Parser |
|---|---|---|
| tailwind.css - 2380419 bytes | 1864728 bytes | 1633188 bytes |
| bootstrap-4.css - 200078 bytes | 153616 bytes | 144571 bytes |
| bootstrap-5.css - 205481 bytes | 159987 bytes | 150852 bytes |
On the complete benchmark suite, css-parser generated a total output size of 2,158,698 bytes, compared to 2,494,113 bytes for Lightning CSS and larger outputs for all other tested minifiers. While some tools prioritize raw execution speed, css-parser focuses on maximizing compression while preserving stylesheet semantics, resulting in consistently smaller production bundles.
css-parser is designed for maximum minification performance, but the optimal configuration depends on what matters most for your workload.
The parser provides settings that let you choose the balance between minification performance and execution speed:
-
Minification performance: prioritize the quality and efficiency of the minification process, allowing the parser to perform additional work when generating the minimized output.
-
Execution speed: minimize the amount of work performed during parsing and minification to achieve the fastest possible execution time.
However, the available settings allow you to shift the balance toward execution speed when minimizing runtime is more important than performing every available minification optimization.
The example below demonstrates a balanced configuration that improves execution speed by disabling advanced minification features.
import {transformSync} from "@tbela99/css-parser";
const css = `...`;
const result = transformSync({
input: css,
minify: false,
beautify: false,
removeEmpty: true,
removeComments: true,
convertColor: true,
minifyValues: true,
});
console.debug(result.code);
console.debug(result.stats);Performance vs execution speed comparison
| File: tailwind.css | Speed optimization | Minification optimization |
|---|---|---|
| Size: 2,380,419 bytes | 1,890,835 bytes | 1,633,188 bytes |
| Time | 128.67 ms | 268.86 ms |
| Size reduction | -20.6% | -31.4% |
Try it online
- Installation
- Usage
- Validation
- CSS Modules
- Minification
- Custom Transform
- Sourcemap
- Plugins API
- Syntax Lowering
- Prefix Removal
- Ast Manipulation
- Arena Memory
- Utility Functions
- Migration to 2.0
Ast nodes and value tokens are allocated in an arena (AstArena) and referenced by their id (a number). The node data is stored
column-wise in typed arrays (structure of arrays) and is read / written through the arena. The parser does not allocate
node or token objects:
import {parse, render, EnumToken} from '@tbela99/css-parser';
const {arena, ast} = await parse(`.foo { color: red }`);
const rule = arena.getFirstChild(ast);
const declaration = arena.getFirstChild(rule);
console.log(arena.getType(rule) == EnumToken.RuleNodeType, arena.getSelector(rule)); // true '.foo'
console.log(arena.getName(declaration)); // 'color'
console.log(arena.tokens(declaration).map((token) => [EnumToken[arena.getType(token)], arena.str(token)])); // [['Color', 'red']]
console.log(render(arena, ast).code); // .foo{color:red}arena.getType(n): EnumToken.CommentNodeTypearena.getValue(n): string, the comment
arena.getType(n): EnumToken.StyleSheetNodeTypearena.children(n): array of children ids
arena.getType(n): EnumToken.DeclarationNodeTypearena.getName(n): string, declaration namearena.tokens(n): array of token ids, the declaration valuearena.getState(n): EnumAstNodeStatus, validation statearena.getErrors(n): ErrorDescription[], validation errors
arena.getType(n): EnumToken.RuleNodeType or EnumToken.KeyframesRuleNodeTypearena.getSelector(n): string, css selectorarena.tokens(n): array of token ids, the parsed selectorarena.children(n): array of children idsarena.getState(n): EnumAstNodeStatus, validation statearena.getErrors(n): ErrorDescription[], validation errors
arena.getType(n): EnumToken.AtRuleNodeType or EnumToken.KeyframesAtRuleNodeTypearena.getName(n): string. AtRule namearena.getValue(n): rule preludearena.tokens(n): array of token ids, the parsed preludearena.children(n): array of children ids (at-rules with a block)arena.getState(n): EnumAstNodeStatus, validation statearena.getErrors(n): ErrorDescription[], validation errors
arena.toObject(id) returns an object view of a node or a token (AstArenaNode): its properties (typ, nam, sel,
val, chi, tokens, loc, state, errors, parent, ...) are getters and setters that read and write the arena.
The view only holds the arena and the node id (view[ARENAID]). Importing a view of the same arena is a noop:
const rule = arena.toObject(arena.getFirstChild(ast));
rule.sel = '.bar';
rule.chi[0].nam = 'background-color';
// the returned arrays are frozen: assign the property to modify the arena
rule.chi = [...rule.chi, {typ: EnumToken.DeclarationNodeType, nam: 'width', val: [{typ: EnumToken.LengthTokenType, val: 3, unit: 'px'}]}];
console.log(arena.fromObject(rule) == rule[ARENAID]); // true: nothing is copied
console.log(JSON.stringify(rule)); // deep plain copyThe traversal and search functions are methods of the arena: arena.walk(), arena.walkValues(), arena.find(),
arena.findAll(), arena.findLast(), arena.findByValue(), arena.findValue(). They return node ids. Their callbacks,
like the visitors, are called with the arena and the node id: everything else is read from the arena.
for (const node of arena.walk(ast)) {
console.log(EnumToken[arena.getType(node)], arena.getParent(node));
}
const result = await transform(css, {
visitor: {
Declaration(arena: AstArena, id: number) {
const node = arena.toObject(id);
if (node.nam == 'height') {
node.nam = 'width';
}
}
}
});A visitor returns nothing to keep the node, or an object or a node id (or an array of objects and node ids) to replace it.
The arena never frees memory by itself: detached nodes keep their memory and their ids stay valid. A long-lived arena
can be compacted. compact() and trim() renumber the nodes and return the mapping of the old ids to the new ids:
console.log(arena.getCapacity(), arena.getLastId(), arena.getStringCount()); // capacity, last allocated id, interned strings
arena.mark(node); // mark a node to be released later
const mapping = arena.trim(result.errors); // release the marked nodes, drop the unreachable nodes, shrink the columns
const newId = mapping[oldId]; // 0 when the node has been droppedSee Arena Memory.
The arena can be moved to a worker (and back) without copying the node data:
// main thread
const [properties, transferables] = AstArena.toTransferableArenaObject(result.arena);
worker.postMessage({ast: result.ast, properties, transferables}, transferables);
// worker
const arena = AstArena.fromTransferableArenaObjects(event.data.properties, event.data.transferables);
console.log(render(arena, event.data.ast).code);See Ast Manipulation.
- Sourcemap generation
- Input sourcemap: when the input CSS comes from another tool, you can pass the sourcemap content to link the generated CSS positions to the original files.
-
all - animation
- background
- border
- border-block-end
- border-block-start
- border-bottom
- border-color
- border-image
- border-inline-end
- border-inline-start
- border-left
- border-radius
- border-right
- border-style
- border-top
- border-width
- column-rule
- columns
- container
- contain-intrinsic-size
- flex
- flex-flow
- font
- font-synthesis
- font-variant
- gap
- grid
- grid-area
- grid-column
- grid-row
- grid-template
- inset
- list-style
- margin
- mask
- offset
- outline
- overflow
- padding
- place-content
- place-items
- place-self
- scroll-margin
- scroll-padding
- scroll-timeline
- text-decoration
- text-emphasis
- transition
- Bundle file referenced by the @import statement.
