
CSS preprocessors like Less, Sass and Stylus have long provided powerful features that vanilla CSS lacked: variables, nesting of rulesets, mixins, control flow constructs, etc. These days however, the feature gap is considerably narrower, and it's not so clear that the benefits of a preprocessor outweight the burdens of setting it up.
It's not terribly hard to set up a CSS preprocessor, but still, it adds complexity to a frontend build system that oftentimes is frightfully complicated to begin with. The project's build system needs to transform files to CSS, which typically involves adding another project dependency (e.g. sass-loader for Sass in Webpack). Quirks with the ways the preprocessor's import system differs from the project's JavaScript module system can cause issues. Getting sourcemaps to work so that styles can be viewed/debugged in the browser can similarly require fiddling. There's also the added cognitive load it places on developers, who will need to learn the preprocessor's ins and outs in addition to those of CSS itself. Projects should be kept as lean as possible, so it's unwise to add additional languages and build tooling unless the benefit they provide is substantial.
As for the benefits, well, it's true that you don't have to look for long before you find one that vanilla CSS still lacks; nesting for example only just recently (as of May 2023) saw support land in Chrome and Safari, with no support yet in Firefox. But there's one extra tool which makes the difference here, and it's PostCSS: with PostCSS, not-yet-supported CSS proposals can be transpiled into widely-supported CSS, which enables enough nice CSS features that you arguably don't need a preprocessor like Sass or Less.
That may seem like a disingenuous take, given that PostCSS itself is a plugin-based CSS preprocessor. But the difference is, you're already using PostCSS anyway. Or at least, you should be, because of plugins like autoprefixer which automatically improve the browser compatibility of your styles. If you've already integrated PostCSS into your project, then it costs nothing (in terms of time and project complexity) to leverage it further.
Here's a quick overview of how vanilla CSS and PostCSS plugins compare to the major selling points of other CSS preprocessors:
- Variables: CSS Variables have wide browser support.
- Nesting: Nesting is a real win for code readability when it's not overused. This is a Stage 2 CSS proposal, meaning it requires a PostCSS plugin and is subject to change, but it's comparable in terms of capabilities to nesting in other preprocessors.
- Mixins and partials: CSS does not provide these concepts, however with variables and classes providing a lot of overlap capability-wise in terms of factoring out common rules, it's debatable whether the added capabilities of mixins/partials are a benefit or an overcomplication.
- Functions and Control Flow: Some preprocessors like Sass provide conditional and looping constructs, or even full-on scripting capabilities. CSS doesn't provide this, but it's another case where arguably this is achieved more cleanly in JavaScript rather than introducing scripting into stylesheets.
- Color Functions: Most fancy ways to manipulate colors are not yet standardized in CSS, but existing proposals cover a lot of what preprocessors offer:
color-mix()is a Stage 2 proposal that enables dynamically mixing colors together.- Relative color syntax is a Stage 2 proposal which allows defining one color relative to another one.
color-contrast()helps choose the color that contrasts the most, though it is an experimental Stage 1 proposal.
- Concatenating class names: Some preprocessors have a syntax where a nested block's selector can be concatenated with the parent's, e.g. to combine
.parentwith__childto form a.parent__childclass in BEM convention rather than writing an un-nested.parent__childselector. CSS doesn't provide this either, but it's a mild convenience that comes at the expense of some clarity.
Overall, variables seem like the key feature that was missing from CSS when preprocessors first gained popularity, but they're widely-adopted now and enable a lot of code reuse possibilities. Nesting is also considered essential by many, which currently requires enabling one Stage 2 proposal via a PostCSS plugin. It may feel scary to enable a proposal whose stage implies "relatively unstable and subject to change", but nesting support has recently landed in Chrome and Safari, so it's looking more standardized than its stage would suggest. And after all, Sass, Less and Stylus all have a history of breaking changes, so a certain level of risk of future breaking changes is inherent to using any of these tools.
In other words, with one PostCSS plugin, you can write plain CSS in 2023 whose clean maintainability rivals what's possible in Sass, Less or Stylus. And there are dozens of other plugins to reach for if your use case ends up calling for something specific (such as fancy color functions).
With this being the case, there's no reason to stop using a preprocessor that's already integrated into your codebase; that would be a time-consuming and regression-prone migration, for little gain. But for a greenfield project in 2023, I really don't think it's worthwhile any longer to complicate a project with yet another language and build step. Why not keep things simple and stick to plain CSS?