Vanilla+PostCSS as an Alternative to SCSS
Tags: css, webdev, programming, tutorial
Published: 2023-03-30 12:52:34

Sass and SCSS have been popular tools to enhance the official CSS syntax, much like TypeScript or CoffeeScript have added features to JavaScript missing in the ["vanilla"](https://en.wikipedia.org/wiki/Vanilla_software) core language specification.

## Would you still use CoffeeScript in 2023?

As a front-end web developer, do you still use [CoffeeScript](https://coffeescript.org) or [jQuery](https://jquery.com)? Unlikely, as [TypeScript](https://www.typescriptlang.org), [ES/TC39](https://tc39.es) and [Babel](https://babeljs.io) (and the retirement of Internet Explorer [thanks to @codepo8 and his EDGE team](https://christianheilmann.com/2023/03/15/8-years-on-edge/)) have helped to transform JavaScript into some kind of a modern programming language.

### Vanilla CSS + PostCSS

Vanilla CSS has taken a similar path with [ambitious working drafts](https://www.w3.org/Style/2011/CSS-process), better browser support, and [PostCSS](https://postcss.org) to fill the gap for user agents lagging behind. So why is Sass/SCSS still so popular? Maybe we go so used to it that we might have forgotten what problems it was meant to solve in the first place.

## Cases for Using Sass in 2023

While we may disagree whether Sass is still relevant today, Mayank's [case for using Sass in 2022](https://blog.mayank.co/the-case-for-using-sass-in-2022) sums up the many use cases for Sass/SCSS including a timeline from 2006 (Sass) to "2022+" (nesting). Nesting CSS used to one of the few good reasons left to choose Sass, SCSS (or [less](https://lesscss.org)) in a new web project.

### Making Code Easier to Read and Maintain

Nesting, variables, extends / mixins, and imports all help us to make our code more readable and easier to maintain. But we can achieve that with or without SCSS, if you don't mind the occasional extra `var(--)` or `calc()`. That's valid CSS without any parsing, so we could even copy and paste between our editor and our browser and reduce the mental load of learning an additional syntax.

![Screenshot of the same code in SCSS and CSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abhvhdlrk8grmvt440q0.png)

### Nesting, Variables, Extends, and Imports

Some features like calculation have long been possible in native CSS, and some other improvements like scoped modules, parent selectors, or container queries, aren't related to Sass, but still worth mentioning for a modern / future CSS setup recommendation using PostCSS.

![Screenshot of a web research: use tomorrow's CSS syntax ... two steps forward, one step back ... deprecated in favor of postcss-preset-env](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j76etjj3i6ilolqy1tub.png)

## Future CSS Configuration beyond CSS next

Switching from a ready-made tool like Sass or a recommendation package like [cssnext](https://cssnext.github.io) (deprecated since 2019) or [PostCSS Preset Env](https://github.com/csstools/postcss-preset-env) (archived in 2022), to the modular [PostCSS Preset Env plugin set](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env) we can choose a helpful and convenient set of future CSS features beyond the current stable client CSS.

## SCSS Variables vs. CSS Custom Properties

Can we use custom properties? Yes, we can!

[![CanIUse.com screenshot: global 96.19% support for CSS Variables (Custom Properties) except for Opera Mini (and Internet Explorer, filtered out after its official obsoletion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyjd7lb6eceetwb3i4fe.png)](https://caniuse.com/?search=custom%20properties)

Unless we can't live without hierarchical objects to structure our design tokens (or use map values to iterate over an abundant number of [adaptive breakpoints](https://css-tricks.com/the-difference-between-responsive-and-adaptive-design/)), native custom properties can replace SCSS variables even without any transpilation.

The native CSS syntax is a bit more verbose though, as we need to use the built-in `var()` function to access custom properties:

```scss
// SCSS
$red = red;
color: $red;
```

```css
/* CSS */
--red = red;
color: var(--red);
```

But to be absolutely sure to ship 100% backwards compatible code, we can use, [postcss-custom-properties](https://www.npmjs.com/package/postcss-custom-properties) a PostCSS) plugin to export static values just like SCSS variables are converted when generating the final CSS code.

`postcss.config.js`:

```javascript
const postcssCustomProperties = require(
  'postcss-custom-properties'
);
module.exports = {
	plugins: [
		postcssCustomProperties({
			preserve: false,
		}),
	]
}
```

Custom properties are expected to be in [kebab-case](https://en.wiktionary.org/wiki/kebab_case) with hyphens, but without dots or hierarchy, at least according to stylelint's recommended [custom-property-pattern](https://stylelint.io/user-guide/rules/custom-property-pattern/). Variable objects like SCSS `$grid-breakpoint.md` could become `--grid-breakpoint--md`, which is good enough in my opinion. Even a complex prefixed design system like the WordPress theme engine is still readable. They even export those values to the client, so maybe I shouldn't worry about converting my `var()` and `calc()` statements anymore as well? Here is an example of a very long property name found in the default WordPress presets palette: `--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray`.

![Screenshot of some default WordPress preset custom properties in the browser developer tools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7e3rvz30gia7usprc5jk.png)

## `calc()` and other built-in Functions

When Sass was released as "syntactically awesome style sheets" in 2006, CSS still lacked many of the features making it so versatile and useful today, like the [`calc()` function](https://developer.mozilla.org/en-US/docs/Web/CSS/calc), which has become so common and [well supported](https://caniuse.com/?search=calc()) that I nearly forgot to mention calculation as an (obsolete) Sass/SCSS feature at all.

Today, Sass calculations are nothing but syntactical sugar allowing us to omit an explicit built-in `calc()` function of native CSS. 

## Future-Proof CSS?

While the SCSS version looks a little bit more compact and elegant, but the native CSS version will work in nearly 100% of your customers's browsers even if you shipped it without any PostCSS postprocessing. It will most probably also work when you'll open the project for bugfixing 5 years later without worrying about that forgotten legacy syntax of Sass, CoffeeScript, or jQuery.

## Importing or Inlining CSS Files

We can write [`@import` in modern CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/@import) much like in SCSS, but CSS will keep distinct files.

The plugin [postcss-import](https://www.npmjs.com/package/postcss-import) provides an easy way to inline all import rules into a single file.

As it is recommended to be run before any other PostCSS plugins, we can insert it before `postcssCustomProperties` in our `postcss.config.js`:

```javascript
const postcssCustomProperties = require('postcss-custom-properties');
module.exports = {
  plugins: [
    require('postcss-import'),
    postcssCustomProperties({
      preserve: false,
    }),
  ]
}
```

## CSS Modules

[CSS modules](https://github.com/css-modules/css-modules) are not to be confused with mixins, as they serve the opposite purpose. While mixins are components or functions to be reused globally, modules are style sheets with a local scope used in a similar way as [styled components](https://styled-components.com) in React.

Using CSS modules is a strategy to ensure that they don't interfere with other CSS, so we could safely use modular projects without cryptic hashes as class names or even IDs. But that's what happens when using [postcss-modules](https://www.npmjs.com/package/postcss-modules), or at least the module classes get prefixed with random hash.

This is not that much different — but less human-readable — than using prefixes matching a namespace assumed to be unique by convention, like an npm module or a WordPress theme or plugin.

Modules are not expected to become part of the official CSS specification in the near future, but I wanted to mention them here mostly to disambiguate modules and mixins.

### Will there ever be a CSS Modules Module?

To add to our possible confusion, "module" has another meaning, as the World Wide Web Consortium (W3C) organizes language specifications and drafts on a modular level, so there are, for example,
- the [CSS Nesting Module](https://www.w3.org/TR/css-nesting-1/),
- the [CSS Color Module Level 3](https://www.w3.org/TR/css-color-3/),
- the [CSS Backgrounds and Borders Module Level 4](https://drafts.csswg.org/css-backgrounds-4/),

and there will probably also be a "CSS Modules Module" in the future.

## Extends, Mixins, and User-Defined Custom Functions

We might think of mixins and SCSS functions as [user-defined custom CSS functions functions](https://github.com/w3c/css-houdini-drafts/issues/1007). SCSS functions can take and process optional parameters and define complex output rules in their `@return` statements.

Composes are a more minimal way to avoid redundant code that exceeds the power of custom properties when it can't be fit into a single line of [shorthand properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties). Although they're not exactly upcoming native CSS without a working draft, there have been similar implementations like [using classes from another CSS module](https://github.com/css-modules/css-modules#composing-from-other-files). We can use [postcss-composes](https://www.npmjs.com/package/postcss-composes) although the plugin has not been updated for several years.

### PostCSS Extend Rule (deprecated?)

Maybe closer to possible future standards (or maybe not, as I heard it became deprecated and the PostCSS plugin is ["mostly unmaintained"](https://github.com/csstools/postcss-extend-rule/issues/16) since 2022), the [PostCSS Extend Rule](https://github.com/csstools/postcss-extend-rule) lets you use the `@extend` at-rule and functional selectors in CSS, following the speculative [CSS Extend Rules Specification](https://jonneal.dev/specs/css-extend-rule/).

We have to tell stylelint about it:

```json
"rules": {
"at-rule-no-unknown": [true,{"ignoreAtRules": ["extend"]}],
```

And we have to make sure that it runs before the nesting plugin, otherwise it will not work at all due to an [issue](https://github.com/postcss/postcss-nested/issues/11) when using both plugins together.

PostCSS' `@extend` implementation has several limitations, but the simple use case is straightforward.

```css
.font-xl {
  font-size: 48px;
  line-height: 1.5;
}

.hero__heading--xl {
  @extend .font-xl;
}
```

**Update May 2023: @extend seems to be deprecated, and I didn't manage to use it anymore. @mixin ([postcss-mixins](https://github.com/postcss/postcss-mixins) seems to be a good alternative, see my comment and code example below.**

### PostCSS Mixins

`@mixin` might seem a little unintuitive and overengineered at first sight, at least for my simple use case, but we can use it like this:

```css
@define-mixin heading--h2 $className {
  $(className) {
    font-family: var(--font-family-heading);
    font-weight: 400;
    font-size: 2rem;
    line-height: 140%;
    font-style: normal;
    @mixin-content;
  }
}

@mixin heading--h2 .intro__keytext__headline {
  margin-bottom: 1rem; /* this is the @mixin-content */
}
```

I installed `npm install --save-dev postcss-mixins`,
added `postcss-mixins` as a PostCSS plugin in `postcss.config.js` between `postcss-import` and `postcss-custom-properties` and told stylelint about the new at-rules:

```json
"at-rule-no-unknown": [true,{
  "ignoreAtRules": [
  "define-mixin",
  "mixin",
  "mixin-content"
]
```

### User Defined Functions

User defined functions might become part of CSS one day, but currently there isn't even a working draft. So I would  contradict my claim at least in this aspect, when I told you to use the [PostCSS define-function](https://github.com/mcattx/postcss-define-function) plugin which mimics a subset of SCSS's function syntax. 

Another, completely different approach, would be using JavaScript functions with the [postcss-functions](https://www.npmjs.com/package/postcss-functions) plugin.

## Future-Proof CSS causing Breaking Changes in SCSS?

When choosing CSS custom properties (`--` and `var(--`) over the (slightly more compact, consistent, and elegant) `$` made our code more valid and future-proof, that was due to the fact that we didn't actually use future CSS yet. Custom properties are approved, shipped, and supported by every major mainstream browser, so we can be 99% sure the same syntax will still be valid and supported 10 years later.

![A fictional future CSS validation success on March 3, 2033](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/joxnye50kxaqa9tvf9k3.png)

### SCSS vs. CSS Nesting

Depending on our coding preference, we might even (try to) write nesting code that is valid SCSS and future CSS at the same time, simply by never omitting a seemingly optional `&` (ampersand) sign before nested declaration. Is it naive to think that's a sustainable solution?

```scss
.hero {
  ::before {} // valid only in SCSS
  &::before {} // valid SCSS and possibly valid future CSS
```

We might worry and argue that [native nesting (CSS Nesting Level 1)](https://www.w3.org/TR/css-nesting-1/) is still a draft and that it behaves differently from SCSS.

There have been [proposals to native CSS nesting](https://developer.chrome.com/blog/help-css-nesting/) that deviate beyond the "safe" SCSS syntax as shown above, requiring additional curly braces or a `@nest` keyword, none of which have made it into the latest CSS nesting working draft in spring 2023.

If we use (and don't overuse) nesting in a conservative way, I wouldn't expect critical problems when maintaining my own projects, but as I hinted before, that would be too idealistic.

### Upcoming (Breaking) Changes to the SCSS Nesting Syntax

Yesterday, Sass wrote about the issues and incompatibilities and possible plans to support native CSS syntax nesting in the future, indicating unintended problems of native nesting due to its current implementation using an implicit `:is` wrapper:

> This changes the specificity: :is() always has the specificity of its most specific selector. [...]
> In the long term, once :is() is supported by 98% of the global browser market share, we’ll start transitioning Sass to emit :is() when resolving Sass nesting. This will make Sass behave like CSS in the first two behavioral incompatibilities. We will consider this a breaking change, and release it as part of a major version release to avoid unexpectedly breaking existing stylesheets. We’ll do our best to make this transition as smooth as possible using the Sass Migrator.
>
> We will not drop our current behavior for &-suffix unless we can come up with a comparably ergonomic way to represent it that’s more compatible with CSS. This behavior is too important to existing Sass users, and the benefit of the plain CSS version is not strong enough to override that.

Source: [https://sass-lang.com/blog/sass-and-native-nesting](https://sass-lang.com/blog/sass-and-native-nesting)

### Nesting can make CSS Harder to Debug

But I have been quite reluctant to adopt SCSS's nesting feature in general anyway.

![Screenshot of CSS in developer tools and the corresponding SCSS code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tfnh35tf1qgha3bpbec.png)

One reason against nested code is that it gets even harder to find the source code for an element that you inspect in your browser's developer tools, although source maps should solve this problem.

### Early Adopters risk Technical Debt 

If we are unlucky, we will swap one non-standard CSS syntax (SCSS) for another (preliminary future CSS draft about to change). That's our risk as early adopters and it's not that hard to refactor our code if we have to. Maybe we don't, as long as our PostCSS module outputs valid CSS to our clients.

### Support Nesting in our CSS Code

As we don't want to ship experimental future code to our clients, we need to configure PostCSS to convert nested code back to valid CSS.

Also we have to configure stylelint by adding [stylelint-use-nesting](https://www.npmjs.com/package/) and extend our configuration.

```
{
  "plugins": [
    "stylelint-use-nesting"
  ],
  "rules": {
    "csstools/use-nesting": "always" || "ignore"
  }
}
```

The `use-nesting` rule has two primary options: "always" means that we must use nesting wherever possible, so I prefer "ignore" which simply does not complain about nesting syntax, but without enforcing it.

If we still see errors and warnings in our editor, they might come from additional tools, like the built-in WebStorm inspections. Like SonarLint, we might just turn them off completely, and leave linting and annotating to our tools (stylelint and eslint) with their explicit project configuration that should work for everyone no matter which tool or platform they prefer.

![Screenshot of PhpStorm Editor Inspections settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0nqeiqvfoiz5fev7p29.png)

## Untranspilable CSS Features

All of the new "future" features until this very point have been nothing but [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) to improve our codebase's developer experience.

It does not matter if a feature is supported by all client browsers, as long as there is a way to generate CSS code that every popular device should understand at least in a basic way.

But there are some modern CSS features that can't be safely used, no matter with or without SCSS or PostCSS.

[Container queries](https://dev.to/ingosteinke/a-css-container-queries-example-1le0) and [parent selectors](https://dev.to/ingosteinke/css-hasparent-selectors-287c) have been two breakthrough achievements of CSS in 2022. There is a [cq-prolyfill](https://github.com/ausi/cq-prolyfill) for container queries, but it seems that there is no simple [polyfill](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill)  or [transpilation](https://en.wiktionary.org/wiki/transcompile) code that can be generated automatically.

So it's up to us as developers to provide fallback code ourselves or if we only use it for [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement) so that our site does not break on older devices.

## Conclusion and Code Example

We can replace most SCSS by native CSS and an appropriate PostCSS and stylelint configuration, but early adoption of CSS drafts like native nesting might introduce technical debt into our project.

Here is a minimal configuration focused on the main features:  custom properties, extends, import, and nesting.

### Node modules:

`npm install --save-dev`

* `postcss`
* `postcss-cli`
* `postcss-custom-properties`
* `postcss-extend-rule`
* `postcss-import`
* `postcss-nesting`
* `stylelint`
* `stylelint-config-standard`
* `stylelint-use-nesting`

### `postcss.config.js`:

```javascript
const postcssCustomProperties = require(
  'postcss-custom-properties'
);
module.exports = {
  plugins: [
    require('postcss-import'),
    postcssCustomProperties({
      preserve: false,
    }),
    require('postcss-extend-rule'),
    require('postcss-nesting'),
  ],
}
```

### `.stylelintrc`:

```json
{
  "extends": "stylelint-config-standard",
    "plugins": [
      "stylelint-use-nesting"
    ],
    "rules": {
    "at-rule-no-unknown": [true,{
      "ignoreAtRules": ["extend"]
    }],
    "csstools/use-nesting": "ignore"
  }
}
```