css-parser
CSS parser and minifier for node and the browser
Installation
From npm
$ npm install @tbela99/css-parser
from jsr
$ deno add @tbela99/css-parser
Features
no dependency
CSS validation based upon mdn-data
fault-tolerant parser, will try to fix invalid tokens according to the CSS syntax module 3 recommendations.
fast and efficient minification without unsafe transforms,
see benchmark
minify colors: color(), lab(), lch(), oklab(), oklch(), color-mix(), light-dark(), system colors and
relative color
generate nested css rules
convert nested css rules to legacy syntax
generate sourcemap
compute css shorthands. see supported properties list below
css transform functions minification
evaluate math functions: calc(), clamp(), min(), max(), etc.
inline css variables
remove duplicate properties
flatten @import rules
experimental CSS prefix removal
Playground
Try it online
Exports
There are several ways to import the library into your application.
Node exports
import as a module
import {transform} from '@tbela99/css-parser' ;
Deno exports
import as a module
import {transform} from '@tbela99/css-parser' ;
import as a CommonJS module
const {transform} = require ('@tbela99/css-parser/cjs' );
Web export
Programmatic import
import {transform} from '@tbela99/css-parser/web' ;
Javascript module from cdn
<script type ="module" >
import {transform} from 'https://esm.sh/@tbela99/css-parser@1.1.0/web' ;
const css = `
.s {
background: color-mix(in hsl, color(display-p3 0 1 0) 80%, yellow);
}
` ;
console .debug (await transform (css).then (r => r.code ));
</script >
Javascript module
<script src="dist/web/index.js" type="module" ></script>
Single Javascript file
<script src="dist/index-umd-web.js" ></script>
Transform
Parse and render css in a single pass.
Usage
transform (css, transformOptions : TransformOptions = {}): TransformResult
Example
import {transform} from '@tbela99/css-parser' ;
const {ast, code, map, errors, stats} = await transform (css, {minify : true , resolveImport : true , cwd : 'files/css' });
TransformOptions
Include ParseOptions and RenderOptions
ParseOptions
Minify Options
minify: boolean, optional. default to true . optimize ast.
pass: number, optional. minification pass. default to 1
nestingRules: boolean, optional. automatically generated nested rules.
expandNestingRules: boolean, optional. convert nesting rules into separate rules. will automatically set nestingRules
to false.
removeDuplicateDeclarations: boolean, optional. remove duplicate declarations.
computeTransform: boolean, optional. compute css transform functions.
computeShorthand: boolean, optional. compute shorthand properties.
computeCalcExpression: boolean, optional. evaluate calc() expression
inlineCssVariables: boolean, optional. replace some css variables with their actual value. they must be declared once
in the :root {} or html {} rule.
removeEmpty: boolean, optional. remove empty rule lists from the ast.
CSS Prefix Removal Options
removePrefix: boolean, optional. remove CSS prefixes.
Validation Options
validation: ValidationLevel | boolean, optional. enable validation. permitted values are:
ValidationLevel.None: no validation
ValidationLevel.Default: validate selectors and at-rules (default)
ValidationLevel.All. validate all nodes
true: same as ValidationLevel.All.
false: same as ValidationLevel.None
lenient: boolean, optional. preserve invalid tokens.
Sourcemap Options
src: string, optional. original css file location to be used with sourcemap, also used to resolve url().
sourcemap: boolean, optional. preserve node location data.
Ast Traversal Options
visitor: VisitorNodeMap, optional. node visitor used to transform the ast.
Urls and @import Options
resolveImport: boolean, optional. replace @import rule by the content of the referenced stylesheet.
resolveUrls: boolean, optional. resolve css 'url()' according to the parameters 'src' and 'cwd'
Misc Options
removeCharset: boolean, optional. remove @charset.
cwd: string, optional. destination directory used to resolve url().
signal: AbortSignal, optional. abort parsing.
RenderOptions
Minify Options
beautify: boolean, optional. default to false . beautify css output.
minify: boolean, optional. default to true . minify css values.
withParents: boolean, optional. render this node and its parents.
removeEmpty: boolean, optional. remove empty rule lists from the ast.
expandNestingRules: boolean, optional. expand nesting rules.
preserveLicense: boolean, force preserving comments starting with '/*!' when minify is enabled.
removeComments: boolean, remove comments in generated css.
convertColor: boolean, convert colors to hex.
Sourcemap Options
sourcemap: boolean | 'inline', optional. generate sourcemap.
Misc Options
indent: string, optional. css indention string. uses space character by default.
newLine: string, optional. new line character.
output: string, optional. file where to store css. url() are resolved according to the specified value. no file is
created though.
cwd: string, optional. destination directory used to resolve url().
Parsing
Usage
parse (css, parseOptions = {})
Example
const {ast, errors, stats} = await parse (css);
Rendering
Usage
render (ast, RenderOptions = {});
Examples
Rendering ast
import {parse, render} from '@tbela99/css-parser' ;
const css = `
@media screen and (min-width: 40em) {
.featurette-heading {
font-size: 50px;
}
.a {
color: red;
width: 3px;
}
}
` ;
const result = await parse (css, options);
console .error (render (result.ast .chi [0 ].chi [1 ].chi [1 ], {withParents : false }));
console .debug (render (result.ast .chi [0 ].chi [1 ].chi [1 ], {withParents : true }));
Merge similar rules
CSS
.clear {
width : 0 ;
height : 0 ;
color : transparent;
}
.clearfix :before {
height : 0 ;
width : 0 ;
}
import {transform} from '@tbela99/css-parser' ;
const result = await transform (css);
Result
.clear , .clearfix :before {
height : 0 ;
width : 0
}
.clear {
color : #0000
}
Automatic CSS Nesting
CSS
const {parse, render} = require ("@tbela99/css-parser/cjs" );
const css = `
table.colortable td {
text-align:center;
}
table.colortable td.c {
text-transform:uppercase;
}
table.colortable td:first-child, table.colortable td:first-child+td {
border:1px solid black;
}
table.colortable th {
text-align:center;
background:black;
color:white;
}
` ;
const result = await parse (css, {nestingRules : true }).then (result => render (result.ast , {minify : false }).code );
Result
table .colortable {
& td {
text-align : center;
&.c {
text-transform : uppercase
}
&:first -child, &:first -child + td {
border : 1px solid #000
}
}
& th {
text-align : center;
background : #000 ;
color : #fff
}
}
CSS Validation
CSS
#404 {
--animate-duration : 1s ;
}
.s , #404 {
--animate-duration : 1s ;
}
.s [type="text" { --animate-duration: 1s;}.s [type="text" ] ]
{
--animate-duration : 1s ;
}
.s [type="text" ] {
--animate-duration : 1s ;
}
.s [type="text" i] {
--animate-duration : 1s ;
}
.s [type="text" s]
{
--animate-duration : 1s
;
}
.s [type="text" b]
{
--animate-duration : 1s ;
}
.s [type="text" b] ,{
--animate-duration : 1s
;
}
.s [type="text" b]
+ {
--animate-duration : 1s ;
}
.s [type="text" b]
+ b {
--animate-duration : 1s ;
}
.s [type="text" i] + b {
--animate-duration : 1s ;
}
.s [type="text" ())] {
--animate-duration : 1s ;
}
.s (){
--animate-duration : 1s ;
}
.s :focus {
--animate-duration : 1s ;
}
with validation enabled
import {parse, render} from '@tbela99/css-parser' ;
const options = {minify : true , validate : true };
const {code} = await parse (css, options).then (result => render (result.ast , {minify : false }));
console .debug (code);
.s :is ([type=text] ,[type=text i] ,[type=text s] ,[type=text i] +b ,:focus ) {
--animate-duration : 1s
}
with validation disabled
import {parse, render} from '@tbela99/css-parser' ;
const options = {minify : true , validate : false };
const {code} = await parse (css, options).then (result => render (result.ast , {minify : false }));
console .debug (code);
.s :is ([type=text] ,[type=text i] ,[type=text s] ,[type=text b] ,[type=text b] +b ,[type=text i] +b ,:focus ) {
--animate-duration : 1s
}
Nested CSS Expansion
CSS
table .colortable {
& td {
text-align : center;
&.c {
text-transform : uppercase
}
&:first -child, &:first -child + td {
border : 1px solid #000
}
}
& th {
text-align : center;
background : #000 ;
color : #fff
}
}
Javascript
import {parse, render} from '@tbela99/css-parser' ;
const options = {minify : true };
const {code} = await parse (css, options).then (result => render (result.ast , {minify : false , expandNestingRules : true }));
console .debug (code);
Result
table .colortable td {
text-align : center;
}
table .colortable td .c {
text-transform : uppercase;
}
table .colortable td :first -child, table .colortable td :first -child + td {
border : 1px solid black;
}
table .colortable th {
text-align : center;
background : black;
color : white;
}
Calc() resolution
import {parse, render} from '@tbela99/css-parser' ;
const css = `
a {
width: calc(100px * log(625, 5));
}
.foo-bar {
width: calc(100px * 2);
height: calc(((75.37% - 63.5px) - 900px) + (2 * 100px));
max-width: calc(3.5rem + calc(var(--bs-border-width) * 2));
}
` ;
const prettyPrint = await parse (css).then (result => render (result.ast , {minify : false }).code );
result
a {
width : 400px ;
}
.foo-bar {
width : 200px ;
height : calc (75.37% - 763.5px );
max-width : calc (3.5rem + var (--bs-border-width) * 2 )
}
CSS variable inlining
import {parse, render} from '@tbela99/css-parser' ;
const css = `
:root {
--preferred-width: 20px;
}
.foo-bar {
width: calc(calc(var(--preferred-width) + 1px) / 3 + 5px);
height: calc(100% / 4);}
`
const prettyPrint = await parse (css, {inlineCssVariables : true }).then (result => render (result.ast , {minify : false }).code );
result
.foo-bar {
width : 12px ;
height : 25%
}
CSS variable inlining and relative color
import {parse, render} from '@tbela99/css-parser' ;
const css = `
:root {
--color: green;
}
._19_u :focus {
color: hsl(from var(--color) calc(h * 2) s l);
}
`
const prettyPrint = await parse (css, {inlineCssVariables : true }).then (result => render (result.ast , {minify : false }).code );
result
._19_u :focus {
color : navy
}
CSS variable inlining and relative color
import {parse, render} from '@tbela99/css-parser' ;
const css = `
html { --bluegreen: oklab(54.3% -22.5% -5%); }
.overlay {
background: oklab(from var(--bluegreen) calc(1.0 - l) calc(a * 0.8) b);
}
`
const prettyPrint = await parse (css, {inlineCssVariables : true }).then (result => render (result.ast , {minify : false }).code );
result
.overlay {
background : #0c6464
}
Node Walker
import {walk} from '@tbela99/css-parser' ;
for (const {node, parent, root} of walk (ast)) {
}
AST
typ: number
val: string, the comment
Declaration
typ: number
nam: string, declaration name
val: array of tokens
Rule
typ: number
sel: string, css selector
chi: array of children
AtRule
typ: number
nam: string. AtRule name
val: rule prelude
AtRuleStyleSheet
typ: number
chi: array of children
KeyFrameRule
typ: number
sel: string, css selector
chi: array of children
Sourcemap
Minification
minify keyframes
minify transform
evaluate math functions calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(),
acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign()
multi-pass minification
inline css variables
merge identical rules
merge adjacent rules
minify colors
minify numbers and Dimensions tokens
compute shorthand: see the list below
remove redundant declarations
conditionally unwrap :is()
automatic css nesting
automatically wrap selectors using :is()
avoid reparsing (declarations, selectors, at-rule)
node and browser versions
decode and replace utf-8 escape sequence
Computed shorthands properties
Performance
Node Transformation
Ast can be transformed using node visitors
Exemple 1: Declaration
the visitor is called for any declaration encountered
import {AstDeclaration , ParserOptions } from "../src/@types" ;
const options : ParserOptions = {
visitor : {
Declaration : (node: AstDeclaration ) => {
if (node.nam == '-webkit-transform' ) {
node.nam = 'transform'
}
}
}
}
const css = `
.foo {
-webkit-transform: scale(calc(100 * 2/ 15));
}
` ;
console .debug (await transform (css, options));
Exemple 2: Declaration
the visitor is called only on 'height' declarations
import {AstDeclaration , LengthToken , ParserOptions } from "../src/@types" ;
import {EnumToken } from "../src/lib" ;
import {transform} from "../src/node" ;
const options : ParserOptions = {
visitor : {
Declaration : {
height : (node : AstDeclaration ): AstDeclaration [] => {
return [
node,
{
typ : EnumToken .DeclarationNodeType ,
nam : 'width' ,
val : [
<LengthToken >{
typ : EnumToken .Length ,
val : '3' ,
unit : 'px'
}
]
}
];
}
}
}
};
const css = `
.foo {
height: calc(100px * 2/ 15);
}
.selector {
color: lch(from peru calc(l * 0.8) calc(c * 0.7) calc(h + 180))
}
` ;
console .debug (await transform (css, options));
Exemple 3: At-Rule
the visitor is called on any at-rule
import {AstAtRule , ParserOptions } from "../src/@types" ;
import {transform} from "../src/node" ;
const options : ParserOptions = {
visitor : {
AtRule : (node : AstAtRule ): AstAtRule => {
if (node.nam == 'media' ) {
return {...node, val : 'all' }
}
}
}
};
const css = `
@media screen {
.foo {
height: calc(100px * 2/ 15);
}
}
` ;
console .debug (await transform (css, options));
Exemple 4: At-Rule
the visitor is called only for at-rule media
import {AstAtRule , ParserOptions } from "../src/@types" ;
import {transform} from "../src/node" ;
const options : ParserOptions = {
visitor : {
AtRule : {
media : (node : AstAtRule ): AstAtRule => {
return {...node, val : 'all' }
}
}
}
};
const css = `
@media screen {
.foo {
height: calc(100px * 2/ 15);
}
}
` ;
console .debug (await transform (css, options));
Exemple 5: Rule
the visitor is called on any Rule
import {AstAtRule , ParserOptions } from "../src/@types" ;
import {transform} from "../src/node" ;
const options : ParserOptions = {
visitor : {
Rule (node : AstRule ): AstRule {
return {...node, sel : '.foo,.bar,.fubar' };
}
}
};
const css = `
.foo {
height: calc(100px * 2/ 15);
}
` ;
console .debug (await transform (css, options));
Exemple 6: Rule
Adding declarations to any rule
import {transform} from "../src/node" ;
import {AstRule , ParserOptions } from "../src/@types" ;
import {parseDeclarations} from "../src/lib" ;
const options : ParserOptions = {
removeEmpty : false ,
visitor : {
Rule : async (node : AstRule ): Promise <AstRule | null > => {
if (node.sel == '.foo' ) {
node.chi .push (...await parseDeclarations ('width: 3px' ));
return node;
}
return null ;
}
}
};
const css = `
.foo {
}
` ;
console .debug (await transform (css, options));