A modern CSS parser and stringifier with TypeScript support
Parse CSS into an Abstract Syntax Tree (AST) and convert it back to CSS with configurable formatting. Built with TypeScript for type safety and modern JavaScript features.
V5 will be the final release of @adobe/css-tools. We recommend moving to
PostCSS directly whenever possible, especially for new projects.
V5 retains V4's public API and css-tools AST. It replaces the parser internals with PostCSS and adds compatibility shims for existing options and default output. The returned AST remains a css-tools AST rather than a native PostCSS AST. The V4 parser is not used at runtime.
Compatibility is verified for the tested corpus, not every CSS input. The upstream parser limitations are accepted for this final release and will not be fixed in css-tools.
Using PostCSS directly is a separate migration: its API and AST differ from css-tools. V5 lets existing applications upgrade without that migration while keeping a path open to using PostCSS directly.
npm install @adobe/css-toolsimport { parse, stringify } from '@adobe/css-tools'
// Parse CSS to AST
const ast = parse('body { font-size: 12px; }')
// Stringify AST back to CSS
const css = stringify(ast)
// => "body { font-size: 12px; }"
// Pretty print with custom indentation
const formatted = stringify(ast, { indent: ' ' })
// => "body {\n font-size: 12px;\n}"
// Minify output
const minified = stringify(ast, { compress: true })
// => "body{font-size:12px}"Parses CSS code and returns an Abstract Syntax Tree (AST).
Parameters:
code(string) - The CSS code to parseoptions(object, optional) - Parsing optionssilent(boolean) - Collect recoverable parse errors instead of throwing. Recovery can still throw for some malformed inputs; see known issues.source(string) - File path for better error reportingstripCommentsInValues(boolean) - Remove comment-like text from declaration values, including inside quoted strings, for V4 compatibility (default:true). Set tofalseto preserve leading, embedded, and trailing value comments.
Returns: CssStylesheetAST - The parsed CSS as an AST
Converts a CSS AST back to CSS string with configurable formatting.
Parameters:
ast(CssStylesheetAST) - The CSS AST to stringifyoptions(object, optional) - Stringification optionsindent(string) - Indentation string (default:' ')compress(boolean) - Whether to compress/minify the output (default:false)
Returns: string - The formatted CSS string
- Complete CSS Support: All standard CSS features including selectors, properties, values, at-rules, and comments
- TypeScript Support: Full type definitions for all AST nodes and functions
- Error Handling: Configurable error handling with detailed position information
- Formatting Options: Pretty print, minify, or custom formatting
- Performance Optimized: Efficient parsing and stringification for large CSS files
- Source Positions: Track original source positions for debugging and tooling
- Selectors: Element, class, ID, attribute, pseudo-class, pseudo-element selectors
- Properties: All standard CSS properties and custom properties
- Values: Colors, lengths, percentages, functions, calc(), etc.
- At-rules: @media, @keyframes, @import, @charset, @namespace, @font-face, @page, @document, @supports, @container, @layer, @starting-style, @host, @custom-media
- Comments: Both /* */ and // comments
- Whitespace: Preserves formatting information
- Vendor prefixes: Supports vendor-prefixed at-rules and properties
- Nested rules: Media queries, supports, containers, etc.
- Complex selectors: Combinators, pseudo-selectors, attribute selectors
import { parse } from '@adobe/css-tools'
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`
// Parse with silent error handling
const result = parse(malformedCss, { silent: true })
// Check for parsing errors
if (result.stylesheet.parsingErrors) {
console.log('Parsing errors:', result.stylesheet.parsingErrors.length)
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`)
})
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length)import { parse } from '@adobe/css-tools'
const css = 'body { color: red; }'
const ast = parse(css, { source: 'styles.css' })
// Position information is available
const rule = ast.stylesheet.rules[0]
console.log(rule.position?.source) // "styles.css"
console.log(rule.position?.start) // { line: 1, column: 1 }
console.log(rule.position?.end) // { line: 1, column: 20 }Positions and parse errors refer to the supplied CSS. As in V4, input source maps
are not loaded or resolved; sourceMappingURL comments remain ordinary CSS comments.
For more examples, see the Examples documentation.
V5's PostCSS-based parser was faster than the published V4.5.0 parser on these samples. Measurements used Benchmark.js with repeated warm, steady-state samples on Node.js 24.18.0 and an Apple M3 Max (macOS/arm64); each figure is the median of three runs. ASTs and stringified outputs were checked for equality before timing. These measurements use PostCSS 8.5.30 and include the final compatibility repairs.
| CSS input | Size | V4.5.0 parse (ms) | V5 parse (ms) | V5 speedup |
|---|---|---|---|---|
| One rule | 20 bytes | 0.0057 | 0.0051 | 1.1x |
benchmark/small.css |
7.8 KB | 0.477 | 0.336 | 1.4x |
| Pure.css 3.0.0 | 26.2 KB | 4.140 | 1.662 | 2.5x |
| Bootstrap 5.3.3 | 281 KB | 621.3 | 24.8 | 25.1x |
For Bootstrap, parsing plus default stringification improved from 633.4 ms to 27.7 ms, about 23x faster. Stringification alone was broadly unchanged: Bootstrap took about 0.76 ms in both V4 and V5.
Warm throughput is not cold-start latency, and small inputs do not always favor
V5. Separate alternating-order, forced-GC batch runs measured a 17-byte rule at
0.0061 ms in V4 and 0.0127 ms in V5, and benchmark/small.css at 0.521 ms in V4
and 0.591 ms in V5. Median module loading, excluding process startup, was
0.65 ms for V4's CJS entry and 8.87 ms for V5's ESM entry across eleven fresh
processes each; the first parse after loading took 0.60 ms and 3.05 ms.
Fresh-process Bootstrap peak RSS was about 72.1 MiB in V4 and 97.9 MiB in V5,
including module initialization, a live AST, and compressed stringification.
The standalone UMD bundle grew from 20,942 to 131,834 bytes, or from 4,701 to
39,296 bytes gzipped.
These are sample measurements, not guaranteed speedups. Results depend on the
CSS, warm-up, garbage collection, runtime, and hardware. See the benchmark/
directory for the existing parser benchmark and its inputs, and
known issues for upstream adversarial-input costs.
The V5 parser uses PostCSS with compatibility shims, not the V4 parser.
css-tools-v4 is a development-only alias pinned to the published
@adobe/css-tools@4.5.0 release as the test oracle.
npm ci
npm test -- --runInBand
npm run build
npm run test:bundles
npm run test:compatibility:publicThe offline suite compares all existing CSS fixtures, lexical/error edge cases, and 1,000 seeded generated inputs. It checks complete AST fields, source positions, non-enumerable parent links, strict and silent errors, and byte-identical default, compressed, and custom-indent output. It also checks that V5 can stringify V4 ASTs. Bundle checks cover CJS, ESM, and standalone UMD browser, CommonJS, and AMD loading. Each loader also parses depth-2048 nested media in an isolated process with a 128 MiB old-space limit and a five-second deadline, checking ASTs, positions, and parent descriptors without recursively walking the result. This gate covers parsing, not deep stringification.
The public-CSS command samples eight complete stylesheets from ten pinned npm
distribution URLs on jsDelivr, using seed 0x405000. It verifies SHA-256 checksums
before testing and stores downloads and their provenance in the gitignored
.compatibility-css/ directory. The sample includes Bootstrap (normal and minified),
Bulma, Animate.css, Pure.css, Milligram, Water.css, and Modern Normalize.
Public files use default parse options and all three formatting modes; the offline
inputs exercise source and silent combinations. The published V4 parser is slow
on the full Bulma file, so this opt-in command can take several minutes.
Passing these comparisons establishes exact parity for the tested inputs, not a proof of identical behavior for every possible CSS string. Additional lexical and malformed-input probes have confirmed differences outside that corpus; see known issues.
The following upstream resource limitations are accepted and will not be fixed in css-tools. They affect both strict and silent parsing.
Core PostCSS at-rule token handling takes superlinear time on headers with
many leading comments, such as '@unknown ' + '/**/ '.repeat(n) + 'x;'.
PostCSS repeatedly removes tokens from the front of an array before the
compatibility layer receives the AST. This is not regex backtracking.
Core PostCSS unclosed url( tokenization searches the rest of the input
for a closing parenthesis at each unclosed url(, so input with many unclosed
url( takes quadratic time. To restore V4's recovery, V5's silent mode can
parse such input up to eight more times, which multiplies this cost:
- On 384 KB inputs like
'a{b:' + 'url(x '.repeat(n) + '}', PostCSS ran two or three times, and V5 reported about 320,000 errors, V4's error for each character V4 skips. Parsing took 1.2–2.5 s. - On 688 KB of
'a{b:url(x;}'.repeat(n), PostCSS ran nine times, and parsing took about 5 s.
postcss-safe-parser alone took about 0.23 s and 0.44 s on those inputs. V4's
silent mode took over 4 s on 12 KB of the first shape and 28 s on 43 KB of the
second.
Safe-parser recovery resource exhaustion occurs when postcss-safe-parser
recursively recovers long sequences without declaration semicolons, such as
'a {' + '\n color: red'.repeat(3100) + '\n}'. Each recursion level copies the
remaining tokens, so this can overflow the stack or exhaust memory before the
compatibility layer receives an AST. With Node 24's default limits, 37 KB of
this shape threw a stack-overflow RangeError after about 0.15 s, 300 KB took
about 1.2 s and 3.4 GB of memory to throw, and 600 KB exhausted the heap, which
aborts the process. V4 parses each in under 5 ms. The thresholds depend on the
runtime and resource limits. This recovery issue is separate from core
PostCSS's token-handling cost.
For untrusted CSS, silent is not an exception-safety or execution-time
guarantee. Limit input size and isolate parsing in a worker or process with an
enforceable timeout and memory limit, such as a Worker with resourceLimits;
an in-process timer cannot interrupt synchronous parsing, and heap exhaustion
cannot be caught.
Applications relying on V4's behavior for these inputs should not treat V5 as a
behaviorally identical upgrade.
Separately, some malformed CSS still parses differently from V4, in strict errors, silent recovery, or both. Known examples:
- After eight re-parses, silent mode keeps PostCSS's reading of any further
malformed input. With nine or more copies of
a{b:(c;}d{e:f}, V5 keeps fewer rules than V4. With nine or more copies ofa{b:url(x;}, it keepsurl(xvalues where V4 reports errors. - V4 reads a nested block at-rule's prelude past the parent's
}into a later block, as in@media y{@media x}b{}}; V5 keeps PostCSS's structure. At the root, V4 also reads some at-rules' preludes past a stray}, as in@foo x}b{c:d}; V5 reads the later block as a separate rule. - V4 accepts unclosed
[in values and selectors, as ina{b:[c;}d{e:f}anda[b{}. - When no
)follows an unclosedurl(in a selector, as inurl(x {}, V4 skips the selector text through the(; V5 keeps it and reports PostCSS'sUnclosed bracketerror. - V4 rejects a block after an
@importnested in a block at-rule, as in@media x{@import y{a:b}}; V5 reads an@importrule. - V4 ends a
@keyframesblock at a nested at-rule or selectorless block, as in@keyframes x{@media x{}}and@keyframes x{{}}, reports a missing}and reads the rest outside the block. V5 keeps the@keyframesblock and drops the nested content, reporting an error only for malformed content, such as a missing selector.
- API Reference - Complete API documentation
- AST Structure - Detailed AST node types and structure
- Examples - Comprehensive usage examples
- Changelog - Version history and changes
This is a fork of the npm css package, maintained by Adobe with modern improvements including TypeScript support, enhanced performance, and security updates. It provides a robust foundation for CSS tooling, preprocessing, and analysis.