So long, and thanks for all the Sass

After 10 happy years, I’m saying goodbye to SCSS

A strange thing just happened. I was starting a new web project and setting up my build pipeline (and yes, I'm the kind of nerd who enjoys that kind of thing), and I didn't import my boilerplate .scss templates. This is probably the first time I've not started a project by importing Sass in, oh, about ten years.

What is Sass/SCSS and why did I need it in the first place?

CSS is how I got into "programming" in the first place (thank you, MySpace!), and I've always loved doing fun and weird and over-engineered things with CSS. But a big part of what made me confident that CSS was a professional tool I could build a career around was being able to enhance my workflow with "pre-processing".

CSS was always powerful, but it came with quirks and edge-cases and a lot of tiny papercuts. Using a preprocesser took a lot of that pain away and made the developer experience delightful. Sass helped me fall in love with frontend development.

Simply put, Sass was a tool that let me write the CSS I wanted to write and would then transform it into a "proper" stylesheet that any web browser could understand. Rather than writing styles.css, I'd name my file styles.scss and get instant access to all sorts of fancy extra features.

Side note: SCSS was the Sass syntax I always preferred, because it more closely resembled vanilla CSS in its formatting - all CSS is valid SCSS, whereas the more uncommon .sass syntax removed braces and semicolons.

Using Sass meant I could use variables for colours and other often-repeated values. I could use logic and perform calculations and write "mixins" that meant I could reuse blocks of functionality without repeating myself too much. And I could nest selectors and break my code into smaller files that were easier to read and maintain.

So much of that sounds trivial nowadays, but back in 2012 when I was first learning about these concepts it felt revolutionary. I'd already been tinkering with CSS for years at that point, but learning Sass was the first time I ever felt more like a "programmer" than a "designer".

Why don't I need to use Sass now?

Over the years my use of Sass evolved to the point where I'd built up a solid "starter kit" of Sass conventions and tricks that I found useful in almost every project. Rather than revisiting a load of decisions and choices with every new project, I could get to "meaningful work" much faster. I'd update the starter kit periodically as I learnt new things or discovered new ways to tackle old problems. But, as is often the case with foundational things that become part of your muscle memory, I tended to be a bit slow in removing deprecated or redundant parts. It "just worked" so why invest time in changing it too much?

In the early 2010s it felt like vanilla CSS moved at a glacially slow pace. After the influx of new features in CSS3, things took a long time to trickle out to all browsers and supporting "legacy" browsers was a big deal. But thankfully things are different now. The switch to "evergreen" browsers (ones that automatically update without their users having to do anything) was a massive step forward, as were the introduction of features like custom properties (a.k.a. variables) and grid systems and imports. CSS is great now, and we can pretty much use all the fun stuff everywhere!

I could probably have dropped my Sass starter-kit a few years ago, and a lot of my work projects have been Sass-free for even longer. But this week was the first time I started a new side project and actively avoided adding Sass. Rather than blindly importing the starter-kit just as I always had done before, I instead refactored the whole thing to use vanilla CSS.

What did I need to change and refactor?

The Sass-to-CSS refactor was delightfully straightforward, but it wasn't just a case of renaming the file extensions.

Comments

The first big find-and-replace job was to tackle code comments. In Sass you can use a double forward slash to mark any following text on the line as a comment just like in JavaScript: // text. But in vanilla CSS you can only use /* text */. Crucially, the vanilla version requires the closing */ at the end of the comment. Honestly this might be the biggest thing I miss from Sass, and I was surprised by how much the closing-tag requirement annoyed me. But on the plus side it's encouraging me to be more diligent about removing lines I'd commented-out "temporarily" (and which had inevitably remained in my code for years).

Variables

In Sass a variable is defined with the $name: value; syntax. 10 years ago this was an absolute game changer, but vanilla CSS has had "custom properties" for years now. Because custom properties are part of the vanilla spec, they can be altered and scoped at run-time, which actually makes them more powerful than Sass variables.

There is a case to be made that complex calculations using variables are better done in Sass because all the work happens at build-time (whereas custom properties are worked out by browsers on the fly when the code is run), but in the vast majority of use cases (and, in fact, in all of mine) CSS custom properties are a better choice.

Aside from having to add a "scope" to the variable declarations (Sass vars can be defined anywhere and everywhere within a SCSS file, whereas CSS custom properties need to be defined within a selector) this was a simple refactor. In fact, this is a change I made a long time ago (back when "dark mode" became a common requirement). Since then, all my colour and type vars have been custom properties, and this recent refactor of my starter kit caught a few extra spacing variables that I had forgotten to convert.

$colour-red: #ff4136;

.foo {
    color: $colour-red;
}
:root {
    --colour-red: #ff4136;
}

.foo {
    color: var(--colour-red);
}

Breakpoints mixin

Early in my Sass days I adopted a breakpoint "mixin" strategy for standardising my responsive breakpoints. A mixin is a reusable block of code that can be included (or "mixed in") throughout a stylesheet, so rather than YOLO'ing a new media query every time I needed one I could use my bp($size) mixin to target specific breakpoints.

// My lengthly (and now completely redundant) breakpoint mixin:
@mixin bp($point) {
    // "min-width" breakpoints (screens getting bigger)
    @if $point == u1 {
        @media (min-width: 960px) {
            @content;
        }
    } @else if $point == u2 {
        @media (min-width: 1200px) {
            @content;
        }
    } @else if $point == u3 {
        // etc...
    }
    // "max-width" breakpoints (screens getting smaller)
    @else if $point == d1 {
        @media (max-width: 1000px) {
            @content;
        }
    } @else if $point == d2 {
        // etc...
    }
}

// The mixin in use:
.related-llm-icon {
    width: 80%;
    @include bp(u2) {
        width: 50%;
    }
}

At first glance, this seems like an ideal use for custom properties. At the expence of a few extra characters (i.e. writing a full @media (min-width... statement) I could have pretty much the same functionallity as with the original mixin. (Beware - this is a trap!)

/* Breakpoint variables: */
/* Note: THIS DOES NOT WORK! */
:root {
    --breakpoint-1: 960px;
    --breakpoint-2: 1200px
}

/* Using a breakpoint: */
/* Note: THIS DOES NOT WORK! */
.responsive-thing {
    width: 80%;
    @media (min-width: var(--breakpoint-2)) {
        width: 50%;
    }
}

⚠️ I was totally wrong about this! You cannot use custom properties in media queries. I spent way longer than I'd care to admit debugging why my media queries weren't being applied.

At the time of writing, I haven't cracked this issue. I'd love to be able to set my breakpoint values in a single place without having to resort to any pre- or post-processing, but for now I'm back to copy/pasting good ol' Magic Numbers.

.responsive-thing {
    width: 80%;
    @media (min-width: 1200px) {
        width: 50%;
    }
}

Imports and partials

I was expecting some difficulty when restructuring my Sass partials into vanilla CSS files, but it was actually the easiest part of the whole process. Vanilla CSS's @import syntax is identical to that of Sass, so all I needed to do was add the .css file extension to each import path.

Importing lots of partials into a single production file was a "big win" when I switched from vanilla to Sass, but looking back at the caniuse compatibility table for @import makes me wonder why I thought I needed Sass for this. I don't regret my choice, as I think build-time bundling is still a good idea (see my later section about bundling) and back in 2012 my Sass pipeline was the only way I knew how to do that kind of thing. Thankfully now (as we'll see in a few paragraphs) there are much simpler ways to bundle and minify your CSS.

Nesting

The ability to nest selectors in Sass was a much bigger deal for me when I started than it is now. Back then, the idea of nesting selectors aligned with my mental model for structuring my style rules. Over time, that mental model has shifted; first to limiting nesting to 2 or 3 levels deep, and then with my adoption of BEM it pretty much disappeared completely.

If you'd asked me last week, I'd have said I hardly ever nest selectors at all anymore. That said, it was interesting to see during this latest refactor exactly how much nesting I still had in my starter kit. Not loads, but enough that I had to spend a bit of time copy/pasting root classes onto the start of selectors. None of this substantially changed the structure of my stylesheets, but maybe a little bit of nesting does still fit with how I think about CSS. With that in mind, I might be open to a future where I set up the tooling to reintroduce nesting into my workflow.

Colour functions

Sass has some build-in functions for manipulating colours, and I used them a lot. The most common ones I used were darken() and lighten, which would take a colour (hex-code, string name, whatever you were using would work) and make it darker or lighter by the specified amount. darken($colour, 20%) was my go-to way to set the colours for hover states and dropshadows and meant I didn't have to manually calculate the new colour values.

I do miss the interoperability of colour formats in Sass (being able to mix and match #ff4136 and red and rgb(255, 65, 54) within the same context was great), but I've gradually retrained myself to simply put in a bit more work when defining my colour variables. In places where I would have used darken() or lighten() I now use hsl(), where the "lightness" value of the hue/saturation/lightness format behaves just the same as using a percentage value in darken or lighten. And with tools like Copilot on hand to do the conversion from hex to hsl for me, it's not even that much extra work.

Maybe don’t just ship it, though...

Despite the fact that you can responsibly ship un-processed CSS files with your project, for any large (normal?) sized project you’d be leaving money on the table if you didn’t put some effort into optimisation.

It’s probably still worth bundling your assets

If you’re not bundling your code, every @import statement means the browser has to make another request. Bundling (a.k.a. smushing all your code together into one Big Chungus of a file) means the browser needs to make fewer requests to your server in order to render your page, so in most cases “fewer requests” means “faster page load”.

Obviously part of the joy of switching to a mostly-vanilla workflow is that you don’t have to faff about with build tools anymore, so adding a bundling step is adding complexity to your project. But the options are so much simpler, quicker, and easier to setup than they were ten years ago.

I recommend esbuild for your bundling/post-processing tasks, but I strongly believe that if you’re already using another tool (for your JavaScript, or images, or whatever) then you should add your stylesheet processing into that tool rather than spend ages refactoring.

Vanilla CSS optimisation and processing is table-stakes for tools like webpack and Parcel and Vite and Snowpack and all the rest... Whichever you choose, you’ll be fine.

And while you’re at it, why not minify the code as well

If you’ve gone to the effort of bundling, it’s generally a trivial extra step to add minification to your build as well. Minification reduces the file size of your CSS by eliminating unnecessary characters (whitespace, line breaks, comments, etc.) without altering the functionality.

Some unminified CSS:

/* Main container style */
.container {
    width: 80%;
    margin: 0 auto;
    padding: 20px;
}

/* Header style */
.header {
    background-color: #f3f3f3;
    padding: 10px;
    font-size: 24px;
    border: 1px solid #d1d1d1;
}

The same code, after being minified *(note that as far as the computers are concerned, they’re the same picture):

.container{width:80%;margin:0 auto;padding:20px}.header{background-color:#f3f3f3;padding:10px;font-size:24px;border:1px solid #d1d1d1}

My esbuild “MVP” config for bundling and minifiying CSS files

My setup uses esbuild, and compared to my previous pipelines is hilariously simple (and replaces ~140 lines of webpack config or ~200 of gulpfile.js if you’re feeling really nostalgic).

// build.js
import { build } from "esbuild";

build({
    entryPoints: ['app.css'],
    outfile: 'out.css',
    bundle: true,
    minify: true,
});

Okay, okay... my real setup is a bit more involved than that. It has flags to handle “development” and “production” builds differently, and will watch files for changes and create source maps and smartly rename multiple source files and a whole load of other useful things.

But the point is: that small snippet is all you need.

Install esbuild and add that build.js file and you’re pretty much good-to-go. Rename the entryPoints and outfile paths to match your folder structure and you can create production-ready CSS by running node build.js.

It’s not as straightforward as shipping vanilla, un-processed CSS files, but on balance probably still worth the effort (and it’s a hell of a lot less effort than it used to be!).

What have I learnt?

My biggest takeaway from this exercise is that I should have done this much sooner! Sass is one of those tools that I'd been using for so long that it had become second nature, and I'd stopped even thinking about it. Let alone thinking about if I still needed it. It's so easy to sleep on these "base layer" parts of my tooling, even though it's something I'd interact with almost every day. Now I need to evaluate the rest of my setup and see if there are any other parts in need of a good spring cleaning.

But either way, Sass was an important part of my developer journey and I'll always have fond memories of us ing it. So long, Sass, and thanks for all the fish.


Related posts

If you enjoyed this article, RoboTom 2000™️ (an LLM-powered bot) thinks you might be interested in these related posts:

Static site generators: Hugo vs Jekyll vs Gatsby vs 11ty

I have an obsession with SSGs, and Eleventy is my favourite (even better than Hugo, Jekyll, and Gatsby).

Similarity score: 83% match. RoboTom says:

Simple is hard

How I learnt to focus on substance over style, and began to rethink my relationship with ‘good design’.

Similarity score: 83% match. RoboTom says:



Signup to my newsletter

Join the dozens (dozens!) of people who get my writing delivered directly to their inbox. You'll also hear news about my miscellaneous other projects, some of which never get mentioned on this site.

    Older post:

    Mapping LLM embeddings in three dimensions

    Published on