Feeling Sassy Again
At 4/19/2024
Since 2015, our team has defaulted to PostCSS as our CSS processor of choice. Overall, we like it a lot. But we’ve recently considered reintroducing Sass to our stack.
Our first taste of PostCSS was the Autoprefixer plugin, which we ran after Sass finished compiling. Since we were already limiting our use of nesting, loops, mixins and extends in Sass, we were able to transition fully to PostCSS with just a few more plugins. Our projects felt leaner and more focused, our compile times improved, and we appreciated philosophically that the syntax adopted by plugins was usually based on a future CSS standard.
But a lot can change in four years. Here’s why we’ve reevaluated the value Sass may add to our projects.
Draft Standards Are Not Standards
Some of the most popular PostCSS plugins are written like polyfills, enabling syntax that is on track to becoming a web standard. In theory, you could remove these plugins once the feature makes it to browsers.
But there’s inherent risk in adopting a syntax from standards that aren’t finalized or implemented. PostCSS users learned this the hard way when it came to color functions.
In 2014, a PostCSS plugin for a color
function was written based on the working draft for CSS Color Module Level 4. But as that draft was revised, the function was renamed to color-mod
, resulting in the original plugin being deprecated and replaced. By 2016, the function was removed entirely from the editor’s draft for the color specification. This meant that it was also removed from popular PostCSS plugin packs like cssnext and its successor Preset Env, confusing users who attempted to upgrade.
There’s value in the idea of polyfilling future standards. But drafted standards are by nature too volatile to depend on. Sass’s proprietary but comparatively stable color functions would have offered the same results with a lot less maintenance.
Compiling Everything Sells Browsers Short
Even a finalized syntax may not always be a good candidate for compilation. Variables are a great example. In Sass, they work like this:
$color-default: #111;
body {
color: $color-default;
}
Code language: SCSS (scss)
Which compiles to this:
body {
color: #111;
}
Code language: CSS (css)
You could recreate the same thing with CSS custom properties:
:root {
--color-default: #111;
}
body {
color: var(--color-default);
}
Code language: CSS (css)
But that’s just the tip of the iceberg when it comes to what CSS custom properties are capable of, as expertly explained by Lea Verou in this excellent talk:
To retain as many of those superpowers as possible, the official PostCSS custom properties plugin adds more CSS than it removes by default:
:root {
--color-default: #111;
}
body {
color: #111;
color: var(--color-default);
}
Code language: CSS (css)
Which is great if you wanted those properties to be accessible. But if your intention was to keep things DRY, it’s probably the opposite of what you’d expect.
Recently, we’ve started adding a Sassy variables plugin to our stack to differentiate variables (which we want to compile) from properties (which we want to expose to the browser so they benefit from the cascade):
/* Compile the project's brand color palette */
$color-blue: #456BD9;
$color-white: #fff;
/* Use brand color as custom property fallback */
a {
color: var(--link-color, $color-blue);
}
/* Define property in different context using brand color */
.Theme--dark {
--link-color: $color-white;
}
Code language: SCSS (scss)
While this solves the problem, it also suggests that there may be benefits in differentiating processor features from browser features… another argument in favor of Sass.
Balancing Modularity and Complexity
“Do one thing, and do it well.” That’s the second of the PostCSS Plugin Guidelines, and it’s served the ecosystem well. The majority of the plugins we use are fast, focused and reliable.
But some features might benefit from an awareness of one another. Mixins, loops, conditionals and variables all output CSS rules based on property values, for example. In cases like this, PostCSS is kind of a mess:
- The most popular plugin for
@for
loops introduces its own Sassy variables, which aren’t entirely compatible with those of the simple variables function. - The most popular
@each
plugin has its own syntax and rules for property output. - Want mixins? There’s the official plugin (which does not support conditionals), the
@property
plugin (no relation to custom properties), or an Advanced Variables plugin that also includes mixins and conditionals.
While each of these plugins may work well on their own, they lack the consistency and interoperability of Sass’s feature set.
Sass Continues to Improve
I can’t in good conscience finish this article without acknowledging the hard work of the Sass community. Its syntax has continued to improve and evolve, it compiles in a fraction of the time it used to, it no longer requires Ruby or C bindings, the documentation is as well-written and approachable as ever, and its community guidelines are excellent.
Our Future CSS Stack
I think for our next project we’ll probably try using Sass for compiled, proprietary features that require some direct authorship… variables, mixins, loops, etc. Then we’ll use PostCSS for automatic transformations, optimizations and polyfills… Preset Env, FOFT Classes, Autoprefixer, minification, etc.
Maybe we’ll allow both .scss
and .css
files, with the latter skipping straight to PostCSS when you don’t need any fancy features. Maybe we’ll find the line isn’t super clear and we’re just using the best of both ecosystems. Time will tell, but I’m excited to find out!
What CSS processors (if any) do you use? What do you like about it? Let us know in the comments!