CSS parser, minifier and validator for node and the browser
npm install @tbela99/css-parser      
CSS parser, minifier and validator for node and the browser
From npm
``shell`
$ npm install @tbela99/css-parser
from jsr
`shell`
$ deno add @tbela99/css-parser
- no dependency
- CSS validation based upon mdn-data
- CSS modules support
- fault-tolerant parser implementing the CSS syntax module 3 recommendations.
- fast and efficient minification without unsafe transforms,
see benchmark
- colors minification: color(), lab(), lch(), oklab(), oklch(), color-mix(), light-dark(), system colors and
relative color
- color conversion to any supported color format
- automatic nested css rules generation
- nested css rules conversion to legacy syntax
- sourcemap generation
- css shorthands computation. see the supported properties list below
- css transform functions minification
- css math functions evaluation: calc(), clamp(), min(), max(), etc.
- css variables inlining
- duplicate properties removal
- @import rules flattening
- experimental CSS prefix removal
See the full documentation at the CSS Parser documentation site
Try it online
There are several ways to import the library into your application.
import as a module
`javascript
import {transform} from '@tbela99/css-parser';
// ...
`
import as a module
`javascript
import {transform} from '@tbela99/css-parser';
// ...
`
import as a CommonJS module
`javascript
const {transform} = require('@tbela99/css-parser/cjs');
// ...
`
Programmatic import
`javascript
import {transform} from '@tbela99/css-parser/web';
// ...
`
Javascript module from cdn
`html
import {transform} from 'https://esm.sh/@tbela99/css-parser@1.3.4/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));
`
Javascript module
`javascript
`
Javascript umd module from cdn
`html
(async () => {
const css =
.table {
border-collapse: collapse;
width: 100%;
}
.table td, .table th {
border: 1px solid #ddd;
padding: 8px;
}
.table tr:nth-child(even){background-color: #f2f2f2;}
.table tr:hover {background-color: #ddd;}
.table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4CAF50;
color: white;
}
;
console.debug(await CSSParser.transform(css, {beautify: true, convertColor: CSSParser.ColorType.OKLCH}).then(r => r.code));
})();
`
`ts
/ parse and render css /
transform(css: string | ReadableStream
/ parse css /
parse(css: string | ReadableStream
/ render ast /
render(ast: AstNode, options?: RenderOptions = {}): RenderResult;
/ parse and render css file or url /
transformFile(filePathOrUrl: string, options?: TransformOptions = {}, asStream?: boolean): Promise
/ parse css file or url /
parseFile(filePathOrUrl: string, options?: ParseOptions = {}, asStream?: boolean): Promise
`
Parse and render css in a single pass.
`javascript
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
`typescript
import {transform} from "../src/node";
import {Readable} from "node:stream";
import type {TransformResult} from '../src/@types/index.d.ts';
const readableStream: ReadableStream
const result: TransformResult = await transform(readableStream, {beautify: true});
console.log(result.code);
`
Include ParseOptions and RenderOptions
#### ParseOptions
> Minify Options
- minify: boolean, optional. default to _true_. optimize ast.
- pass: number, optional. minification passes. 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 modules Options
- module: boolean | ModuleCaseTransformEnum | ModuleScopeEnumOptions | ModuleOptions, optional. css modules options.
> 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 | 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().
`javascript
parse(css, parseOptions = {})
`
``javascript
const {ast, errors, stats} = await parse(css);
``
`javascript`
render(ast, RenderOptions = {});
Rendering ast
`javascript
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}}
`
CSS modules features are fully supported. refer to the CSS modules documentation for more information.
`javascript
import {transform} from '@tbela99/css-parser';
const css =
.table {
border-collapse: collapse;
width: 100%;
}
.table td, .table th {
border: 1px solid #ddd;
padding: 8px;
}
.table tr:nth-child(even){background-color: #f2f2f2;}
.table tr:hover {background-color: #ddd;}
.table th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4CAF50;
color: white;
};
const result = await transform(css, {module: true});
// css code
console.log(result.code);
// css mapping
console.log(result.mapping);
`
`javascript
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
`css`
.hsl {
color: color(srgb .7019607843137254 .13333333333333333 .13333333333333333/50%)
}
CSS
`css
.clear {
width: 0;
height: 0;
color: transparent;
}
.clearfix:before {
height: 0;
width: 0;
}
`
`javascript
import {transform} from '@tbela99/css-parser';
const result = await transform(css);
`
Result
`css
.clear, .clearfix:before {
height: 0;
width: 0
}
.clear {
color: #0000
}
`
CSS
`javascript
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
`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
}
}
`
CSS
`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
`javascript
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);
`
`css`
.s:is([type=text],[type=text i],[type=text s],[type=text i]+b,:focus) {
--animate-duration: 1s
}
with validation disabled
`javascript
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);
`
`css`
.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
`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
`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
`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;
}
`
`javascript
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
`css
a {
width: 400px;
}
.foo-bar {
width: 200px;
height: calc(75.37% - 763.5px);
max-width: calc(3.5rem + var(--bs-border-width) * 2)
}
`
`javascript
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
`css
.foo-bar {
width: 12px;
height: 25%
}
`
`javascript
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
`css
._19_u :focus {
color: navy
}
`
`javascript
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
`css
.overlay {
background: #0c6464
}
`
`javascript
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
- [x] sourcemap generation
- [x] minify keyframes
- [x] minify transform functions
- [x] evaluate math functions calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(),
acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign()
- [x] minify colors
- [x] minify numbers and Dimensions tokens
- [x] multi-pass minification
- [x] inline css variables
- [x] merge identical rules
- [x] merge adjacent rules
- [x] compute shorthand: see the list below
- [x] remove redundant declarations
- [x] conditionally unwrap :is()
- [x] automatic css nesting
- [x] automatically wrap selectors using :is()
- [x] avoid reparsing (declarations, selectors, at-rule)
- [x] node and browser versions
- [x] decode and replace utf-8 escape sequence
- [x] experimental CSS prefix removal
- [ ] ~all~
- [x] animation
- [x] background
- [x] border
- [ ] border-block-end
- [ ] border-block-start
- [x] border-bottom
- [x] border-color
- [ ] border-image
- [ ] border-inline-end
- [ ] border-inline-start
- [x] border-left
- [x] border-radius
- [x] border-right
- [x] border-style
- [x] border-top
- [x] border-width
- [x] column-rule
- [x] columns
- [x] container
- [ ] contain-intrinsic-size
- [x] flex
- [x] flex-flow
- [x] font
- [ ] font-synthesis
- [ ] font-variant
- [x] gap
- [ ] grid
- [ ] grid-area
- [ ] grid-column
- [ ] grid-row
- [ ] grid-template
- [x] inset
- [x] list-style
- [x] margin
- [ ] mask
- [ ] offset
- [x] outline
- [x] overflow
- [x] padding
- [ ] place-content
- [ ] place-items
- [ ] place-self
- [ ] scroll-margin
- [ ] scroll-padding
- [ ] scroll-timeline
- [x] text-decoration
- [x] text-emphasis
- [x] transition
- [x] flatten @import
Ast can be transformed using node visitors
the visitor is called for any declaration encountered
`typescript
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));
};
const result = await transform(css, options);
console.debug(result.code);
// .foo{transform:scale(calc(40/3))}
`
the visitor is called only on 'height' declarations
`typescript
import {AstDeclaration, EnumToken, LengthToken, ParserOptions, transform} from '@tbela99/css-parser';
const options: ParserOptions = {
visitor: {
Declaration: {
// called only for height declaration
height: (node: AstDeclaration): AstDeclaration[] => {
return [
node,
{
typ: EnumToken.DeclarationNodeType,
nam: 'width',
val: [
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))
};
const result = await transform(css, options);
console.debug(result.code);
// .foo{height:calc(40px/3);width:3px}.selector{color:#0880b0}
`
the visitor is called on any at-rule
`typescript
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);
}
};
const result = await transform(css, options);
console.debug(result.code);
// .foo{height:calc(40px/3)}
`
the visitor is called only for at-rule media
`typescript
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);
}
};
const result = await transform(css, options);
console.debug(result.code);
// .foo{height:calc(40px/3)}
`
the visitor is called on any Rule
`typescript
import {AstAtRule, ParserOptions} from "../src/@types";
import {transform} from "../src/node";
const options: ParserOptions = {
visitor: {
Rule(node: AstRule): AstRule {
node.sel = '.foo,.bar,.fubar'
return node;
}
}
};
const css =
.foo {
height: calc(100px * 2/ 15);
} ;
const result = await transform(css, options);
console.debug(result.code);
// .foo,.bar,.fubar{height:calc(40px/3)}
`
Adding declarations to any rule
`typescript
import {AstRule, parseDeclarations, ParserOptions, transform} from '@tbela99/css-parser';
const options: ParserOptions = {
removeEmpty: false,
visitor: {
Rule: async (node: AstRule): Promise
if (node.sel == '.foo') {
node.chi.push(...await parseDeclarations('width: 3px'));
return node;
}
return null;
}
}
};
const css =
.foo {
};
const result = await transform(css, options);
console.debug(result.code);
// .foo{width:3px}
``