playground npm npm cov Doc NPM Downloads bundle size
CSS parser and minifier for node and the browser
From npm
$ npm install @tbela99/css-parser
from jsr
$ deno add @tbela99/css-parser
- 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
- convert colors to any supported color format
- generate sourcemap
- compute css shorthands. see supported properties list below
- minify css transform functions
- evaluate math functions: calc(), clamp(), min(), max(), etc.
- inline css variables
- remove duplicate properties
- flatten @import rules
- experimental CSS prefix removal
Try it online
There are several ways to import the library into your application.
import as a module
import {transform} from '@tbela99/css-parser'; // ...
import as a module
import {transform} from '@tbela99/css-parser'; // ...
import as a CommonJS module
const {transform} = require('@tbela99/css-parser/cjs'); // ...
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.3.1/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.js" type="module"></script>
Single Javascript file
<script src="dist/index-umd-web.js"></script>
Parse and render css in a single pass.
transform(css: string | ReadableStream<string>, transformOptions: TransformOptions = {}): TransformResult parse(css: string | ReadableStream<string>, parseOptions: ParseOptions = {}): ParseResult; render(ast: AstNode, renderOptions: RenderOptions = {}): RenderResult;
import {transform} from '@tbela99/css-parser'; const {ast, code, map, errors, stats} = await transform(css, {minify: true, resolveImport: true, cwd: 'files/css'});
Read from stdin with node using readable stream
import {transform} from "../src/node"; import {Readable} from "node:stream"; import type {TransformResult} from '../src/@types/index.d.ts'; const readableStream: ReadableStream<string> = Readable.toWeb(process.stdin) as ReadableStream<string>; const result: TransformResult = await transform(readableStream, {beautify: true}); console.log(result.code);
Include ParseOptions and RenderOptions
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.
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 | ColorType, convert colors to the specified color. default to ColorType.HEX. supported values are:
- true: same as ColorType.HEX
- false: no color conversion
- ColorType.HEX
- ColorType.RGB or ColorType.RGBA
- ColorType.HSL
- ColorType.HWB
- ColorType.CMYK or ColorType.DEVICE_CMYK
- ColorType.SRGB
- ColorType.SRGB_LINEAR
- ColorType.DISPLAY_P3
- ColorType.PROPHOTO_RGB
- ColorType.A98_RGB
- ColorType.REC2020
- ColorType.XYZ or ColorType.XYZ_D65
- ColorType.XYZ_D50
- ColorType.LAB
- ColorType.LCH
- ColorType.OKLAB
- ColorType.OKLCH
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().
parse(css, parseOptions = {})
const {ast, errors, stats} = await parse(css);
render(ast, RenderOptions = {});
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); // print declaration without parents console.error(render(result.ast.chi[0].chi[1].chi[1], {withParents: false})); // -> width:3px // print declaration with parents console.debug(render(result.ast.chi[0].chi[1].chi[1], {withParents: true})); // -> @media screen and (min-width:40em){.a{width:3px}}
import {transform, ColorType} from '@tbela99/css-parser'; const css = ` .hsl { color: #b3222280; } `; const result: TransformResult = await transform(css, { beautify: true, convertColor: ColorType.SRGB }); console.log(result.css);
result
.hsl { color: color(srgb .7019607843137254 .13333333333333333 .13333333333333333/50%) }
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 }
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
#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 }
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; }
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) }
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% }
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 }
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 }
import {walk} from '@tbela99/css-parser'; for (const {node, parent, root} of walk(ast)) { // do something }
- typ: number
- val: string, the comment
- typ: number
- nam: string, declaration name
- val: array of tokens
- typ: number
- sel: string, css selector
- chi: array of children
- typ: number
- nam: string. AtRule name
- val: rule prelude
- typ: number
- chi: array of children
- typ: number
- sel: string, css selector
- chi: array of children
- sourcemap generation
- minify keyframes
- minify transform functions
- evaluate math functions calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign()
- minify colors
- minify numbers and Dimensions tokens
- multi-pass minification
- inline css variables
- merge identical rules
- merge adjacent rules
- 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
- experimental CSS prefix removal
-
(削除) 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
- flatten @import
Ast can be transformed using node visitors
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)); // .foo{transform:scale(calc(40/3))}
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: { // called only for height declaration height: (node: AstDeclaration): AstDeclaration[] => { return [ node, { typ: EnumToken.DeclarationNodeType, nam: 'width', val: [ <LengthToken>{ typ: EnumToken.LengthTokenType, 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)); // .foo{height:calc(40px/3);width:3px}.selector{color:#0880b0}
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)); // .foo{height:calc(40px/3)}
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 => { node.val = 'all'; return node } } } }; const css = ` @media screen { .foo { height: calc(100px * 2/ 15); } } `; console.debug(await transform(css, options)); // .foo{height:calc(40px/3)}
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)); // .foo,.bar,.fubar{height:calc(40px/3)}
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)); // .foo{width:3px}