Library / SDK
thysultan/stylis avatar
thysultan/stylis

stylis: a CSS preprocessor that hands you an AST and lets you write the plugins

light – weight css preprocessor

1,770 stars89 forksJavaScriptMIT

At a glance

What is it?
stylis compiles CSS into an abstract syntax tree of plain JavaScript objects and lets you transform that tree with middleware, which is why it can do nesting, namespacing, prefixing and minification without shipping a runtime. The interesting part is the AST shape, and the interesting limitation is how it handles exotic CSS custom properties.
Who is it for?
stylis fits a library author who needs to author CSS in JavaScript, or a framework author who wants to control namespacing, prefixing and minification itself rather than accept a tool's opinion, and it fits a runtime that must be small enough to ship in a component library. It does not fit a project that wants a Sass-like language with variables, mixins and functions, because the transformations it performs are a fixed set rather than a language.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A preprocessor whose output is a tree of plain objects

The name says it all: a Light-weight CSS Preprocessor. What makes it different from the usual preprocessor is that its intermediate representation is not hidden, it is the product.

The compile function takes CSS text and returns an array of plain objects. The abstract syntax structure section gives the four shapes.

js
const declaration = {
	value: 'color:red;',
	type: 'decl',
	props: 'color',
	children: 'red',
	line: 1, column: 1
}

A declaration has a decl type, the property name in props and the value in children. A comment has type comm with props set to the slash. A rule set has type rule and holds an array of selectors in props. And an at-rule set has type @media with the media query conditions in props.

The point of publishing this structure is that it makes the tool composable. A CSS-in-JS library that needs a different prefixing policy, or a different namespacing scheme, or a different minifier, can implement that by walking the tree, and does not need to fork the parser. The features list is short, nesting, selector namespacing, vendor prefixing, minification, esm module compatible and tree-shaking-able, and the middleware system is what makes the list extensible.

The simplest call shows the pipeline.

js
import {compile, serialize, stringify} from 'stylis'

serialize(compile('h1{all:unset}'), stringify)

Compile produces the tree, serialize walks it with a callback, and stringify is the callback that writes CSS back out. Every feature is implemented as a different callback, and the built in prefixer is itself just a middleware.

js
import {compile, serialize, stringify, middleware, prefixer } from 'stylis';

serialize(compile('div{display:flex;}'), middleware([prefixer, stringify]))

Middleware, and the two different parents you have to keep straight

The middleware helper is described as a convenient utility that, for all intents and purposes, you can do without if you intend to implement your own traversal logic. The stringify middleware is one such middleware that can be used in conjunction with it.

The distinction that matters is between the two parent references, and the documentation is precise about it. Elements passed to middlewares have a root property that is the immediate root or parent of the current element in the compiled output, so it references the parent in the already expanded CSS-like structure. Elements also have a parent property that is the immediate parent of the current element from the input structure, the structure representing the input string.

So root is where you are after expansion, parent is where you were before it. Any plugin that rewrites selectors needs to know which question it is asking.

The traversal example asserts the relationship directly.

js
serialize(compile('h1{all:unset}'), middleware([(element, index, children) => {
	assert(children === element.root.children && children[index] === element.children)
}, stringify])) === 'h1{all:unset;}'

The callback signature is element, index, children, and the children array is the same object as the element's own children, so a middleware can rewrite in place or replace.

The abstract syntax tree also includes an additional return property for more niche uses, and the prefixing example shows what it is for. A middleware can set element.return to a string that is emitted in addition to the element's own value, which is how a plugin injects a fallback declaration without discarding the original.

js
serialize(compile('h1{all:unset}'), middleware([(element, index, children, callback) => {
	if (element.type === 'decl' && element.props === 'all' && element.children === 'unset')
		element.return = 'color:red;' + element.value
}, stringify])) === 'h1{color:red;all:unset;}'

The traversal example also shows the return value being used to substitute a node outright, where a middleware returns a serialized array for a different selector set and the original is emitted afterwards.

js
serialize(compile('h1{all:unset}'), middleware([(element, index, children, callback) => {
	if (element.type === 'rule' && element.props.indexOf('h1') > -1)
		return serialize([{...element, props: ['h2', 'h3']}], callback)
}, stringify])) === 'h2,h3{all:unset;}h1{all:unset;}'

The repository ships tangible examples in src/Middleware.js, and the documentation notes that you could create your own middleware system as compile returns all the necessary structure to fork from. That is the strongest statement in the project about its design, and it is the reason to pick it over a preprocessor that gives you a black box.

Three installation routes, and a package built for tree shaking

There are three ways to get it, and they are listed together because the direct download and the CDN exist for people who are not using a bundler at all.

Use a Direct Download, with a script tag pointing at stylis.js. Use a CDN, with a script tag pointing at unpkg.com/stylis. Or use NPM with npm install stylis --save.

The package metadata explains the distribution story for a bundler user. The type is module, the main field points at a UMD build, the module field at an ESM build, and there is a react-native entry. The exports map has an import condition pointing at the ESM entry and a require condition pointing at the UMD build, plus an explicit package.json export.

The sideEffects field is false, which is the flag that makes the tree-shaking-able claim work. A bundler can drop the module entirely if it can prove nothing uses it, and more importantly it can drop individual exports if the library is written so that importing one function does not pull in the others. Combined with the middleware design, where prefixing and minification are separate functions you import or do not import, that is a coherent approach to size.

The repository also measures size deliberately. The dev dependencies include rollup-plugin-size, and the build script runs rollup with that plugin configured, so the bundle size is checked as part of the build rather than being an afterthought. The README links a bundlephobia size badge for the minified package.

The build itself is rollup, with a prebuild step that removes the dist directory first, and a postversion step that pushes the tags and publishes to npm. The release scripts are split into major, minor and patch, each running npm version with the matching flag and a message.

Custom properties, and an honest note about where the specification goes undefined

The variables section is the most interesting documentation in the repository, because it is the one place where the author explains a limit rather than a feature.

CSS variables are supported, but there is a note to be made about the exotic use of CSS variables. The specification is quoted, and the point being made is that the allowed syntax for custom properties is extremely permissive, since the declaration-value production matches any sequence of one or more tokens, so long as the sequence does not contain certain bad tokens.

Which means, as the documentation puts it, CSS variables according to the spec allow a value like --foo: if(x > 5) this.width = 10;, and while this value is obviously useless as a variable, and would be invalid in any normal property, it still might be read and acted on by JavaScript, and this is supported by Stylis.

The problem appears when braces are involved. The example given is --foo: {}. That is valid CSS and supported, but it is unclear what should happen when the rule collides with the implicit block termination rule that allows a declaration such as h1{color:red} with the semicolon omitted to also be a valid CSS production. The documentation lays out the contradiction in both directions, asking whether h1{--example: {} should be treated as an unterminated block, and whether in h1{--foo:{} h1{color:red;} the part after the braces is part of the CSS variable or a new rule.

The resolution is a general rule rather than a fix. Nevertheless Stylis still supports the exotic forms highlighted in the spec, however you should consider it as a general rule to delimit such exotic uses of variables in strings or parentheses, for example h1{--foo:'{'} or h1{--foo:({)}.

That is an honest resolution. The specification is ambiguous, the library parses the ambiguous forms anyway, and the answer is to quote the braces if you mean them literally.

What stylis is not, and where a real preprocessor wins

The limits are clear from the feature list, and the first one is that this is not a language.

The features are nesting, selector namespacing, vendor prefixing, minification, ES module compatibility and tree shaking. There is no variable system beyond CSS custom properties, no mixins, no functions, no control flow, no imports of other files. If you are looking for something that lets you write a design system in a language with abstractions, this is not it.

The second limit is where it sits relative to a runtime CSS-in-JS library. A runtime library generates style objects and injects them into the document at runtime. stylis generates a string of CSS, which means you can use it in a build step, a server side render path, or a component that ships CSS rather than JavaScript. The middleware design is what makes it adaptable to either, but the output is CSS text and the target is a stylesheet.

The third is that the tool is small, and small means it does not attempt to be a general CSS implementation. A full preprocessor is a large program with a parser, a type system, a module graph and an ecosystem of plugins. stylis is a parser plus a tree walker plus a set of middleware, and the price is that anything beyond the listed features you write yourself.

There is also a version consideration. The repository has no GitHub releases, so the npm version is the version, and it is 4.4.0. The README says Stylis is at-least 2X faster than its predecessor, which is a comparison against the previous major version of the same library rather than a claim against other tools, so it should be read that way.

The closest alternative to name is not a different library but the same design done differently. Emotion and styled-components take a runtime approach with a style object model, and they bring their own parsing and their own runtime cost. stylis takes the build time approach with a string in and a string out, and it costs nothing at runtime because there is no runtime.

Editorial conclusion

stylis fits a library author who needs to author CSS in JavaScript, or a framework author who wants to control namespacing, prefixing and minification itself rather than accept a tool's opinion, and it fits a runtime that must be small enough to ship in a component library. It does not fit a project that wants a Sass-like language with variables, mixins and functions, because the transformations it performs are a fixed set rather than a language. Check three things before you commit. Check the AST contract, because the compile function returns plain objects with value, type, props, children, line and column fields, and everything you build has to work with that shape. Check the middleware contract, because the root property refers to the parent in the compiled output while the parent property refers to the parent in the input structure, and confusing the two is the most common way to write a broken plugin. And check how you will handle custom properties, since the documentation is candid that the exotic forms the CSS specification permits are supported but ambiguous once braces are involved. The last push was on 2026-04-19, the package version is 4.4.0, and the license is MIT.

Frequently asked questions

How do I install stylis?

With npm install stylis --save, or by using a direct download with a script tag pointing at stylis.js, or a CDN such as unpkg.com/stylis. The package is ES module compatible, and the metadata sets sideEffects to false so a bundler can tree shake it.

What is the AST stylis produces?

An array of plain objects. A declaration has type decl with the property in props and the value in children, a rule set has type rule with an array of selectors in props, a comment has type comm, and an at-rule set has type @media with the conditions in props. Every object also carries line and column.

What is the difference between the root and parent properties in stylis middleware?

root is the immediate parent of the current element in the compiled output, so it references the parent in the already expanded CSS-like structure, while parent is the immediate parent from the input structure, the one representing the input string.

How do I vendor prefix CSS with stylis?

Import compile, serialize, stringify, middleware and prefixer, then call serialize(compile('div{display:flex;}'), middleware([prefixer, stringify])). Prefixing is implemented as a middleware, which is why it can be swapped for a different implementation.

How does stylis handle exotic CSS custom property values?

It supports the exotic forms the CSS specification allows, but the documentation notes that things become ambiguous once braces are involved, and advises delimiting such values in strings or parentheses, for example h1{--foo:'{'} or h1{--foo:({)}.

What license is stylis released under?

MIT, with a LICENSE file at the root of the repository, and the package version is 4.4.0. The last push to the repository was on 2026-04-19.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. thysultan/stylis on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thysultan-stylis.svg)](https://hysenlabs.com/projects/thysultan-stylis)