Skip to content

Repository files navigation

playground npm npm cov Doc NPM Downloads bundle size

css-parser

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.

Installation

From npm

$ npm install @tbela99/css-parser

from jsr

$ deno add @tbela99/css-parser

Features

  • 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.
  • @import flattening to produce self-contained stylesheets.
  • 51.5kb once minified and gzipped.

Vendor prefix removal

Vendor prefix cleanup to modernize generated CSS.

Syntax lowering

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.

Benchmark

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.

Minification performance vs execution speed

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%

Playground

Try it online

Documentation

AST

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}

Comment

  • arena.getType(n): EnumToken.CommentNodeType
  • arena.getValue(n): string, the comment

StyleSheet

  • arena.getType(n): EnumToken.StyleSheetNodeType
  • arena.children(n): array of children ids

Declaration

  • arena.getType(n): EnumToken.DeclarationNodeType
  • arena.getName(n): string, declaration name
  • arena.tokens(n): array of token ids, the declaration value
  • arena.getState(n): EnumAstNodeStatus, validation state
  • arena.getErrors(n): ErrorDescription[], validation errors

Rule and KeyframesRule

  • arena.getType(n): EnumToken.RuleNodeType or EnumToken.KeyframesRuleNodeType
  • arena.getSelector(n): string, css selector
  • arena.tokens(n): array of token ids, the parsed selector
  • arena.children(n): array of children ids
  • arena.getState(n): EnumAstNodeStatus, validation state
  • arena.getErrors(n): ErrorDescription[], validation errors

AtRule and KeyframesAtRule

  • arena.getType(n): EnumToken.AtRuleNodeType or EnumToken.KeyframesAtRuleNodeType
  • arena.getName(n): string. AtRule name
  • arena.getValue(n): rule prelude
  • arena.tokens(n): array of token ids, the parsed prelude
  • arena.children(n): array of children ids (at-rules with a block)
  • arena.getState(n): EnumAstNodeStatus, validation state
  • arena.getErrors(n): ErrorDescription[], validation errors

Object views

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 copy

Traversal and visitors

The 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.

Arena memory

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 dropped

See Arena Memory.

Workers

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

  • 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.

Computed shorthands properties

  • 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

Performance

  • Bundle file referenced by the @import statement.

Architecture

./architecture.png

Releases

Packages

Used by

Contributors

Languages