The Issue with Preprocessing CSS Custom Properties

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:

  1. The native handling is exactly like the preprocessor handling
  2. No other part of the preprocessing process is confused by them
  3. 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);
}