CSS has Custom Properties now. We’ve written about them a bunch lately. Browser support is good, but of course, old non-evergreen browsers like Internet Explorer don’t have them and never will. I can see the appeal of authoring with “future CSS”, and letting a preprocessor backport it to CSS that is compatible with older browsers. Babel for CSS! Why not?!
It makes me nervous though – because it’s only some use cases of Custom Properties that you can preprocess. There are plenty of situations where what you are doing with a Custom Property just isn’t possible to preprocesses. So if you do, you’re putting yourself in a pretty weird situation.
If you’re in a situation where you can preprocess them and get what you expect, you probably should have just used preprocessor variables.
Preprocessors can’t understand the DOM structure
It isn’t until “runtime” when a complete DOM is constructed that the CSS applies to. Not to mention the likely scenario that any given CSS file is one-of-many affecting the page.
postcss-custom-properties is perhaps the most popular of the Custom Property preprocessors (it’s bundled with cssnext), and it only allows you to set variables in the :root
, like:
:root {
--backgroundColor: white;
}
.header {
background-color: var(--backgroundColor);
}
But perhaps one of the most useful things about CSS Custom Properties is that they can be used in the cascade. This is a contrived example, but you’ll see the point:
:root {
--backgroundColor: white;
}
.header {
background-color: var(--backgroundColor);
}
.header.is-about-page {
--backgroundColor: yellow;
}
Using postcss-custom-properties, it will process the root variable, but not the state variation, leaving you with some pretty useless CSS:
.header {
background-color: white;
}
.header.is-about-page {
--backgroundColor: yellow;
}
They are aware of this:
The transformation is not complete and cannot be (dynamic cascading variables based on custom properties relies on the DOM tree). It currently just aims to provide a future-proof way of using a limited subset (to
:root
selector) of the features provided by native CSS custom properties. Since we do not know the DOM in the context of this plugin, we cannot produce safe output.
You can read interesting conversations about all this, like this one.
There is another Custom Properties processor called postcss-css-variables that attempts to do more. For example:
.header {
background-color: var(--backgroundColor, white);
}
.header:hover {
--backgroundColor: orange;
}
.header.is-about-page {
--backgroundColor: yellow;
}
Which outputs:
.header {
background-color: white;
}
.header:hover {
background-color: orange;
}
It could handle the hover properly but still gave up on the stated selector.
Both plugins agree: there is just no way to perfectly replicate what CSS Custom Properties can do in a CSS preprocessor.
So if you do it, you either have to hamstring yourself on how you use them, or get incorrect results.
You lose the ability to change them with JavaScript
Besides the cascade, the other killer feature of CSS Custom Properties is being able to change them with JavaScript. So rather than query the DOM with JavaScript for the X things you need to change and change them all individually, you can just change a Custom Property have that percolate through as you expect it would.
By preprocessing CSS Custom Properties away, they don’t exist in the processed CSS, and this you lose this ability.
When do you turn it off?
Perhaps you’re just thinking of this preprocessing as a stopgap. At some point you’ll turn it off, then ship Custom Properties natively. You’ll need to do some work to make sure:
- The native handling is exactly like the preprocessor handling
- No other part of the preprocessing process is confused by them
- Your fallback situation is solid, if you still need a workable experience in browsers that don’t support them
🙃
Sorry, I know this feels like I’m crapping on someone else’s thing. Actually, I think explorations like these are super cool and worth doing and sharing. The people that make them are indisputably smarter than I am.
Some people are stoked about this. Mike Street:
I work for an agency where cross-browser support is a must and that includes IE11 (unfortunately). Although we can’t quite use CSS variables in production, they offer many advantages to using them in development and post-processing them to their original properties.
Our gulp process includes postcss-css-variables which changes any CSS variables in your stylesheets to the values you set them to. Similar to SCSS variables (in the same way they get processed) but to allows you to write smaller SCSS and, when the time comes, remove the processing and deploy your stylesheets with custom properties already in place.
I just think it’s worth sharing a little caution. There is a huge banner on cssnext’s site that says “Use tomorrow’s CSS syntax, today.” and I worry that kind of marketing is selling something as simple that is unfortunately complicated and nuanced.
If you’re preprocessing Custom Properties, you might as well use actual prepreprocessor variables
That seems like a smart call to me, anyway.
Sass, Less, and Stylus all have variables that work great. They even have some sense of scope, if not as powerful as the real casacde.
If you’re into the PostCSS thing, there are plugins that allow variables that use a syntax that doesn’t overlap native CSS syntax.
Mike Street had a pretty cool use case in which he flipped the direction of a gradient at a media query by making a small change to a CSS Custom Property (which is a super useful thing they can do).
div {
--direction: to bottom;
background: linear-gradient(
var(--direction),
rgba(0, 0, 0, 1) 0,
rgba(0, 0, 0, 0.1) 100%);
@media (max-width: 1000px) {
--direction: to right;
}
}
Only postcss-css-variables (I think) will try to process this. It’s nicely succinct, but it is possible to get just about the same nice level of abstraction with SCSS.
@mixin specificGradient($direction) {
background: linear-gradient(
$direction,
rgba(0, 0, 0, 1) 0,
rgba(0, 0, 0, 0.1) 100%
);
}
div {
@include specificGradient(to bottom);
@media (max-width: 1000px) {
@include specificGradient(to right);
}
}
Use Them Both / Handle Fallbacks Now
You can totally use any preprocessor and CSS Custom Properties.
You might even leave some things, like your $brandColor
or something, as a preprocessor variable. Then at the same time, make use of CSS Custom Properties for things you can imagine someday being dynamic. You can even handle fallbacks right away, so you’re dealing with cross-browser support as you go rather than thinking of that as something you’re preprocessing away.
$brandColor: #f06d06;
html {
background: $brandColor;
--base-font-size: 100%;
font-size: 100%;
font-size: var(--base-font-size);
}
Yes, indeed, I’ve always used SCSS-like variables when I have to preprocess values, for these exact reasons. Nice sum up.
They work differently, they’re actually used differently, so just use different tools to manipulate them (SCSS vs. CSS/JavaScript).
Great point, Chris. As a SCSS user myself, I’ve been wrestling with this question too. That is, “Where can I use Custom Properties instead of SCSS variables?” and, more importantly, “When can I start using them?”
Like you’ve pointed out, Custom Properties are most powerful when they take advantage of the cascade. I’d sum that up as: “Custom Properties are best for any value that may change.”
I’m especially looking forward to using Custom Properties for column gutters, heading font-sizes, and other responsive variables. The opportunities to make our CSS much more concise and efficient are really exciting for me.
Im using them in production with SCSS with the help of the css-vars sass mixin or alternatively you can do a simple sass fallback:
https://vgpena.github.io/winning-with-css-variables/
Doubt IE will support them
I think, one relevant aspect that wasn’t mentioned in the article is, what can preprocessor variables do that custom properties can’t? I can think of property name interpolation, but what else is there?
If a site does not use any of these extra features, then is there any reason not to switch to custom properties? The
var()
syntax is a bit tedious, but that’s a small price to pay for “going standard.”In other words, if browser support doesn’t allow you to use the special features of custom properties, and you don’t need the special features of preprocessor variables, then choosing either is really just a matter of personal preference, no?
Another esoteric thing is the ability to strip units from preprocessor variables.
But more importantly, the two Custom Properties preprocessors mentioned have different ways of handling you doing things that they don’t support. Even in my simple tests, one left in some Custom Properties code that wouldn’t do anything anymore (since it preprocessed other parts), and the other stripped it all out. So you need to be hyper-aware of how it handles perfectly valid browser-usage of CSS Custom Properties, our end up with some processed code that has yanked it out or broken it.
I just wondered if it would be possible to have a main.css which uses custom properties and multiple other files of which just one is loaded per page/category/whatever and in these files the custom properties get their values.
ex.:
main.css:
home.css:
contact.css:
main.css would be used for every page and the files home.css and contact.css only to the corresponding pages.
I use custom properties over a year with cssnext and never felt like it is a huge problem. You only loose the flexibility of defining variables inside media queries, making you write overrides instead of just changing a variable. But this isn’t something that slows you down. I feel like I can live with going a step back, closer to CSS and gain sustainable code in exchange.