The Trouble With Preprocessing Based on Future Specs

Let's say there are rumblings from the deep (read: early Editor's Drafts of potential future web tech specifications) that demonstrate some potential future code syntax. Say that syntax looks pretty awesome and we want it to be useable right now. That's the idea behind some preprocessing these days.

Because this style of preprocessing has the vibe "write the code of the future and turn it into the code of today" it's been popular to call them a postprocessor. I think that's silly at best and confusing at worst. I'm going to keep calling it preprocessing. The process is exactly the same as preprocessing: you write code in a special syntax, it's processed into regular CSS in a build step, the regular CSS is what the site uses. Preprocessing. I think postprocessing would be a better word for things like -prefix-free or other client-side polyfills.

The conversations of future-syntax preprocessing in CSS are connected to conversations about PostCSS. But PostCSS actually doesn't do much CSS transformation on its own. David Clark explains it like this:

[PostCSS] does not alter your CSS. A plugin might do that. Or maybe it won't. PostCSS plugins can do pretty much whatever they want with the parsed CSS. One plugin could enable variables, or some other useful language extension. Another could change all of your as to ks. Another could log a warning whenever you use an ID selector. Another could add Masonic ASCII art to the top of your stylesheets. Another could count all the times you use float declarations.

So the future-syntax preprocessing doesn't come directly from PostCSS, it comes from PostCSS plugins. There is one plugin in particular that focuses on this: cssnext. cssnext is actually a collection of PostCSS plugins that are all focused on future-syntax preprocessing. But cssnext is not alone. There are other preprocessors and plugins that operate like this. On the JavaScript side, preprocessors like Babel are in the same boat.

I see a few problems with the idea of future-syntax preprocessing.

But to be clear before we get started: you do you. If you use and love preprocessing like this, that's great. Feel free to talk about it in the comments. And if you work on these preprocessor projects, wonderful. You're probably helping the web more than I ever will by allowing people to play with future syntaxes early.

We support PostCSS (including cssnext) on CodePen. We support Babel on CodePen. The last thing in the world I want to do is hamper ideas.

I just think there are a few things to consider here that might cause trouble for folks picking them up to use on real, long-term, production products. At least, it's worth having a conversation about.

One Tiny Example

At the risk of getting bogged down with just one sample, here's a sample of how future-syntax preprocessing might work in CSS. This is how CSS variables is looking in an Editor's Draft right now:

:root {
  --brandColor: #F06D06;
}
.main-header {
  background: var(--brandColor);
}

There is a variety of CSS preprocessor thingies™ that will process that for you. The result is likely:

.main-header {
  background: #F06D06;
}

Problem #1: Future Syntax Changes

Firefox is actually shipping with support for the above example of native CSS variables right now. But they've had to do some weaving. It first landed in v29 with the var- prefix instead of --. But then the spec changed. So they had to change the implementation.

That means CSS preprocessing that was supporting this had to do some weaving too. That means some tough changes and forks-in-the-road for the preprocessor authors.

Choices for the preprocessor developers

  • Do you support the new syntax when it changes?
    • Yes = keeps in the spirit of being a future-syntax preprocessor.
    • No = now you're just a preprocessor based on an orphaned syntax.
  • Do you support the old syntax too?
    • Yes = runs the risk of a confusing syntax and a bloated code base that has to handle things multiple ways.
    • No = you'll break people's code.

Not great choices. When cssnext faces changes, they output warnings first, then deprecate the old syntax in the next version.

cssnext: Previously @custom-selector were working with and without pseudo syntax ':'. Now you must use '@custom-selector :--{name}' syntax instead of '@custom-selector --{name}'. The support of syntax without ':' and this warning will be remove in the next major release.

Choices for the preprocessor users

If you're a user of a future-syntax preprocessor, you have to accept that you'll face breaking changes that come along somewhat randomly. That's at odds with how most other preprocessors work, where the syntax is made up and purposely different from native CSS syntax (future or not).

  • Do you update code to the new syntax or not? If the preprocessor developers deprecate the old syntax, you either have to update your code or never update versions.
  • Do you have a choice? If the preprocessor developers stay with the old syntax or support both versions, you could leave your code alone. You'll have to decide how dedicated you are to the new syntax. If the developers go new-syntax-only, you'll have to decide to either update your code or give up and stop that particular bit of preprocessing.

Problem #2: Some Features Can't Be Replicated

The CSS variables example we started with can demonstrate this as well. The way native CSS variables are going to work in the browser isn't the same as just replacing variable names with values in the final CSS. With native CSS variables, if you change the value of the variable with JavaScript, everything referencing that variable will update accordingly. It remains dynamic in a way that replacing those values with static code would not.

CSS calc() is another example. You can't just replace all instances with calc() with a processed value. What 100% - 20px computes to is impossible to know by a preprocessor. The browser needs to evaluate that, and keep evaluating it as the the environment changes. If you just need to to some static math, you should probably just do some math and not leave it inside calc().

The PostCSS plugin for calc() is smart in that it "reduces calc() references whenever it's possible." - meaning if what you have written could have been static, it makes it static. That goes to show that while we're having a wide-in-scope conversation here, a discussion about usage on a particular project would be more narrow-in-scope, focusing on how particular plugins solve particular needs you have.

The plugin for separating the transform properties says specifically:

Once these new properties are supported natively, you can also use them to style transforms across multiple rules without overwriting a previous rule's transforms. Unfortunately, I cannot predict how your CSS rules will be inherited without also knowing your DOM. Therefore, this particularly useful feature cannot be simulated at this time.

So if you start using this now, you don't get the primary benefit of the syntax existing. And when you remove the plugin, the code will function entirely differently.

It's not that the plugin is bad. It does good stuff. It makes authoring more intuitive. It makes version control difs more useful. It signals interest to browser feature implementers there is interest here. It alters code to use the most performant techniques.

It's just feels a little dangerous and like you better know what you're doing if you go down this road.

Problem #3: The Cutover

If the reason you're attracted to using future-syntax processors is that you think that someday you can ditch the preprocessor and have totally valid CSS, that's going to be tough. It might actually delay that moment. Say you want to start actually shipping some future-syntax code because some browsers are starting to support it. But your preprocessor is still processing it, and you can't remove the preprocessor because a lot of browsers aren't supporting it yet.

Maybe the preprocessor can start outputting code that does both the future syntax and a fallback? Maybe that's impossible? Maybe it could be the developer is unresponsive or uninterested?

And what if a future syntax never comes about? Then the cutover may never happen.

Personally I'm OK with never cutting over. I think a level of CSS abstraction (the preprocessing step) makes sense to always be there. You need a build step anyway (concatenation, compression, other deployment-prep) might as well preprocess as well.

Plus there are concepts that I think belong in an abstraction of a language and not the language itself. Variables could be an example here again. Preprocessor variables and native variables could co-exist and be useful in their own ways. If native CSS could do everything ever dreamed up in a preprocessor, it would be slow, complicated, and likely wouldn't have seen the success that CSS has had as a language.

Problem #4: Code Portability

I've heard it expressed that future-syntax code is safer to write because it's just CSS and therefore more portable. Meaning you can share it with others, share it between projects, write about it more easily, etc.

If we're talking about current, native CSS, that would be true. But remember that PostCSS is a large plugin ecosystem and you use it by piecemealing together plugins you're interested in using. That means any given project is using a different configuration of plugins. Any given chunk of code may not work in a different project using different plugins. Or it might work if it's only reliant on plugins that the other project happens to also have. Or it might work differently if the other project has different plugins but happens to accept that same authored syntax.

It seems like code is actually less portable when written in a system like this, as compared to code written in a system with universal abstractions that are purposefully different than native CSS, future or otherwise. It seems people highly involved in both Sass and CSS agree on this.

So

What do you think? Am I totally wrong that there is anything to be worried about here? Do you have any personal experience to share?