PostCSS plugin to transform CSS Custom Properties (CSS variables)
syntax into a static representation. This plugin provides a future-proof way of using most of CSS variables features, including selector cascading with some caveats, because this can only see the CSS, not the potentially dynamic HTML and DOM the CSS is applied to.
npm install postcss-css-variables --save-dev
Try it in the playground and see what you think! Just add some CSS and see to see the final transformed/compiled CSS. You can try anything here in the playground, too.
For more general PostCSS usage, look here.
var postcss = require('postcss');
var cssvariables = require('postcss-css-variables');
var fs = require('fs');
var mycss = fs.readFileSync('input.css', 'utf8');
// Process your CSS with postcss-css-variables
var output = postcss([
cssvariables(/*options*/)
])
.process(mycss)
.css;
console.log(output);
A custom property is any property whose name starts with two dashes --
. A property must be in a rule.
Note: :root
is nothing more than the selector for the root DOM node. Any other selector like .class
, #id
, or even #foo ~ .bar > span.baz
works.
:root {
--foo-width: 100px;
--foo-bg-color: rgba(255, 0, 0, 0.85);
}
.foo {
--foo-width: 100px;
--foo-bg-color: rgba(255, 0, 0, 0.85);
}
Custom properties can be declared multiple times, but like variable scope in other languages, only the most specific one takes precedence.
:root: {
--some-color: red;
}
.foo {
/* red */
color: var(--some-color);
}
.bar {
--some-color: blue;
/* blue */
color: var(--some-color);
}
.bar:hover {
--some-color: green;
/* Automically gets a `color: green;` declaration because we `--some-color` used within scope elsewhere */
}
W3C Draft: CSS Custom Properties for Cascading Variables, section 2
.foo {
width: var(--foo-width);
/* You can even provide a fallback */
background: var(--foo-bg-color, #ff0000);
}
W3C Draft: CSS Custom Properties for Cascading Variables, section 3
It's perfectly okay to declare CSS variables inside media queries and the like. It'll work just as you would expect.
:root {
--width: 100px;
}
@media (max-width: 1000px) {
:root {
--width: 200px;
}
}
.box {
width: var(--width);
}
Will be transformed to:
.box {
width: 100px;
}
@media (max-width: 1000px) {
.box {
width: 200px;
}
}
Psuedo-classes are also dealt with correctly, because it's easy to statically determine.
.foo {
--foo-color: red;
color: var(--foo-color);
}
.foo:hover {
--foo-color: green;
}
Will be transformed to:
.foo {
color: red;
}
.foo:hover {
color: green;
}
This pairs very well with postcss-nested
or postcss-nesting
, adding support for nested rules. For either one, you must use that plugin before postcss-css-variables
in your setup so the &
references can be expanded first (postcss-css-variables
doesn't understand them). For example, with postcss-nested
, your PostCSS setup can look like this:
var postcss = require('postcss');
var cssvariables = require('postcss-css-variables');
var nestedcss = require('postcss-nested');
var fs = require('fs');
var mycss = fs.readFileSync('input.css', 'utf8');
var output = postcss([
// Flatten/unnest rules
nestedcss,
// Then process any CSS variables
cssvariables(/*options*/)
])
.process(mycss)
.css;
console.log(output);
For a simple example with nesting:
.box-foo {
--some-width: 150px;
width: var(--some-width);
.box-bar {
width: var(--some-width);
}
}
With also postcss-nesting
, this will be transformed to:
.box-foo {
width: 150px;
}
.box-foo .box-bar {
width: 150px;
}
For a more complex example with a media query:
:root {
--some-width: 150px;
}
.box-foo {
width: var(--some-width);
.box-bar {
width: var(--some-width);
}
}
@media (max-width: 800px) {
.box-foo {
--some-width: 300px;
}
}
This will be transformed to:
.box-foo {
width: 150px;
}
.box-foo .box-bar {
width: 150px;
}
@media (max-width: 800px) {
.box-foo {
width: 300px;
}
.box-foo .box-bar {
width: 300px;
}
}
It also pairs well with postcss-calc
to reduce calc()
expressions arising from using variables.
var postcss = require('postcss');
var cssvariables = require('postcss-css-variables');
var calc = require('postcss-calc');
var fs = require('fs');
var mycss = fs.readFileSync('input.css', 'utf8');
var output = postcss([
// Process any CSS variables
cssvariables(/*options*/)
// Then reduce `calc()` expressions
calc()
])
.process(mycss)
.css;
console.log(output);
:root {
--page-margin: 1em;
}
/* Easy centering */
.wrapper {
width: calc(100% - 2 * var(--page-margin));
margin-left: var(--page-margin);
margin-right: var(--page-margin);
}
This will be transformed to:
.wrapper {
width: calc(100% - 2em);
margin-left: 1em;
margin-right: 1em;
}
This plugin was spawned out of a discussion on the cssnext
repo and a personal need.
There is another similar plugin available, postcss-custom-properties
, although it restricts itself much more than this, preferring spec conformance over imperfect feature support.
If you are/were already using postcss-custom-properties
, this should work out of the box, without issue, and other than differences in the JavaScript API, it's a drop-in replacement. The only difference in CSS handling is that this attempts much broader support, and you can just start taking advantage immediately. Note the caveats, in that although this does correctly support what postcss-custom-properties
does, there are certain edge cases it doesn't get perfectly.
In postcss-custom-properties
, CSS variable declarations are specifically restricted to the :root
selector. In postcss-css-variables
, this is not the case and they may be declared inside any rule with whatever selector.
Here's a quick overview of the differences:
- CSS variables may be declared in any selector like
.foo
or.foo .bar:hover
, and is not limited to just:root
- CSS variables may be declared in
@media
,@support
, and other at-rules. - CSS variables may be declared in
:hover
and other psuedo-classes, and they are evaluated properly. - Variables in nested rules can be deduced with the help of
postcss-nested
orpostcss-nesting
.
Continue to the next section to see where some of these might be unsafe to do. There are reasons behind the ethos of why the other plugin, postcss-custom-properties
, is very limited in what it supports, due to differing opinions on whether broader, yet potentially incorrect, support is okay.
When you declare a CSS variable inside one selector, but consume it in another, this does make an unsafe assumption about it which can be non-conforming in certain edge cases. Here is an example where these limitations result in non-conforming behavior.
Using the following CSS:
.component {
--text-color: blue;
}
.component .header {
color: var(--text-color);
}
.component .text {
--text-color: green;
color: var(--text-color);
}
When nesting the markup like this, you may get incorrect behavior, because this only knows the CSS inheritance, not the HTML structure. (Note the innermost <h1 class="Title">
.)
<div class="component">
Black
<h1 class="title">
Blue
<p class="decoration">
Green
<h1 class="title">Blue with this plugin, but green per spec</h1>
</p>
</h1>
</div>
This spec deviation is intentional, as there's nothing this tool can do about that. postcss-custom-properties
avoids this problem entirely by restricting itself to just the :root
selector. This is because the developers there would prefer to not support a feature instead of supporting it almost-correctly like what this plugin does.
Allows you to preserve custom properties & var() usage in output.
Possible values:
false
: Removes--var
declarations and replacesvar()
with their resolved/computed values.true
: Keepsvar()
declarations in the output and has the computed value as a fallback declaration. Also keeps computed--var
declarations.'computed'
: Keeps computed--var
declarations in the output. Handy to make them available to your JavaScript.
Define an object map of variables in JavaScript that will be declared at the :root
scope.
Can be a simple key-value pair or an object with a value
property and an optional isImportant
bool property.
The object keys are automatically prefixed with --
(according to CSS custom property syntax) if you do not provide it.
var postcss = require('postcss');
var cssvariables = require('postcss-css-variables');
postcss([
cssvariables({
variables: {
'--some-var': '100px',
'--other-var': {
value: '#00ff00'
},
'--important-var': {
value: '#ff0000',
isImportant: true
}
}
})
])
.process(css, opts);
- This plugin was spawned out of a discussion on the
cssnext
repo - We provide a larger CSS variable feature subset than
postcss-custom-properties
. - Related links and issues:
- var declared in media query should pull in properties that use/reference that var on
cssnext/cssnext
- Investigate support for media-query scoped properties on
postcss/postcss-custom-properties
- remove
:root
limitation by injecting rules with new declarations that just contains modified properties. onpostcss/postcss-custom-properties
- var declared in media query should pull in properties that use/reference that var on
We have a suite of Mocha tests. If you see something that doesn't have coverage, make an issue or pull request.
Run once:
npm install
Run whenever you want to test:
npm run test