Implementation: Do you still need Sass today?

Probably not, but not because of Tailwind

Jérôme Beau
6 min readJun 27, 2024

While CSS3 officially started in 1999, its features only shipped roughly a decade later into browsers. This is why the Sass preprocessor was still useful in those days, allowing to use an advanced styling syntax.

Sass was created in 2006 (its competitor Less in 2009) and helped many frontend developers to write decent, more maintainable styling code. It still does, despite the raise of the infamous Tailwind.

In the meantime, the CSS standard pursued his merry way at a slower pace, reducing the gap between Sass and the standard, and sometimes even going beyond. As of today, CSS support in browsers has reached significant milestones that are worth looking at, before using Sass or any other tool.

For each of the issues mentioned, I will describe the problem, how we dealt with it before (using Sass typically), and how we can handle it today, using modern CSS.

Imports

One important feature of any programming language is the ability to modularize, i.e. split source code into distinct files that will improve both modularity (and so encapsulation, reusability) and readability, as well as reducing the risk of VCS conflicts.

However, loading multiple modules means handling multiple files, which could impair performance. Conversely, reducing the number of requests should not impair the ability to lazy-load stylesheets (i.e. load them only when code requiring them is loaded).

Before

Strictly speaking, you could have your CSS split in multiple files since the beginning, using basic HTML:

<head>
<link rel="stylesheet" href="main.css/>
<link rel="stylesheet" href="utils.css/>
<link rel="stylesheet" href="component.css/>
</head>

But that would imply one HTTP request per CSS file (and so the performance overhead), which is more than you’d want.

Tools (including frameworks build tools) would allow you to merge all those file contents into one (hopefully minified) to warrant a single fetch. But this would also mean loading too much, and prevent the other optimization of lazy loading.

Sass featured an “@import” statement but with detrimental duplication side-effects in some cases, which motivated the introduction of “@use”.

Today

  • You can @import CSS files from other CSS files. So the example above could be replaced by just an import of main.css which would itself contain two @import rules for instance. That would still require as many HTTP requests, but according to the dependency graph.
  • You can also import CSS files from JavaScript modules (to load web components style, for instance), using the ?raw suffix or, in browsers which support it), using import assertions.
  • You still need build tools (like vite) to package chunks of lazy-loadable dependencies.

Variables

Using constants in stylesheets, like in any programming language, raises a number of issues:

  • when you want to change a value, you have to replace constants everywhere (instead of just changing the value of a variable) ;
  • constants with the same value might not have the same meaning (i.e. 16px might be set for both a margin and a width, but you only want to change one of them).
  • you cannot give semantics to a constant: comments are not supported in CSS, and the only way to convey the meaning of a value is through a variable name.

Because of this, variables support is required.

Before

Sass has helped to associate values with meaning by introducing variables through the $ prefix:

$colorAlert: red;
.alert-text {
color: $colorAlert;
}
.alert-button {
background-color: $colorAlert;
}

However, it just replaces variables by their values at preprocessing time:

.alert-text {
color: red;
}
.alert-button {
background-color: red;
}

You won’t be able to change such “variables” at runtime.

Today

“CSS variables” (actually Custom Properties) implement the concept at the core of the rendering engine.

:root {
--color-alert: red;
}
.alert-text {
color: var(--color-alert);
}
.alert-button {
background-color: var(--color-alert);
}

As for Sass, their value can be redefined depending on context:

:root {
--color-alert: red;
}
.alert-text {
color: var(--color-alert);
}
.alert-button {
background-color: var(--color-alert);
}
.warning {
// Redefine the alert color when inside a warning element
--color-alert: orange;
}

But we go beyond Sass here: changing the value of the variable dynamically has an immediate impact on rendering 👍

As you can see above, a variable’s value can be overridden depending on any styling context (a nested class, whatever you can specify using a selector), but that’s also something which can be updated dynamically from JavaScript code:

style = getComputedStyle(someElement)
if (style.getPropertyValue("--color-alert") === "orange") {
someElement.style.setProperty("--color-alert", "yellow");
}

Computations

Sometimes you may need to compute values like sizes (margin, padding, width, height) or business values.

Before

You can compute in Sass but, hey, it’s static/before runtime.

Today

the calc() function allows dynamic computations with % and variables. You can mix different types like 16px + 20% - 2em

Nesting

As “Cascading” indicates, CSS is about scope and inheritance. However it didn’t allow to declare rules in a given scope, thus leading to numerous repetitions in selectors:

.login-form {
padding: 1em;
}
.login-form .email, .login-form .password {
display: block;
}
.login-form footer button {
padding: 1em;
}

Before

Sass brought the nesting feature:

.user-form {
input {
position: absolute;
}
.name {
margin: 1em;
&.first-name {
left: 1em;
}
&.last-name {
left: 20em;
}
}
}

Far more more readable… but the generated CSS is still the same, and sometimes worse:

.user-form .first-name {
left: 1em;
}
.user-form .last-name {
left: 20em;
}

Today

The CSS standard had a spec proposal about nesting as soon as 2015, but was not adopted. Instead, some new pseudo-selectors were proposed as part of CSS4: :any(selectors), replaced by :matches(selectors), finally replaced by :is(selectors) or :where(selectors).

Hopefully, the rejected spec was replaced by another one which has landed in all modern browsers 🎉. Just use the & nesting selector. It still suffers from two limitations however:

  • you cannot nest pseudo-element (::before, etc.) rules, for performance reasons.
  • nesting is not supported (yet) in web components style 😖.

Extends

Sometimes you want to define a style as a specialized version of another.

Before

Sass had the @extendskeyword, but it had its own issues.

Today

The :is(.someAncestor) selector is standard and more powerful, allowing to match more complex selectors.

Mixins

Sometimes you want to repeat CSS code without the “extend” semantics, thus leading to duplicated, less maintainable and error-prone code.

Before

Sass provided mixins as kind of function inlining CSS code where they are called.

The “@apply” rule allowed to do this, and mixins parameters would be replaced by the use of CSS variables:

:root {
--brand-color: red; /* Default value */
--header-theme: {
color: var(--brand-color);
font-family: cursive;
font-weight: 600;
};
}
h1 {
@apply --header-theme;
}
h2 {
--brand-color: green;
@apply --header-theme;
}

but it raised other potential issues and so was deprecated.

Today

@apply has been replaced by a renewal of the CSS shadow ::part pseudo-element which allows to redefine the whole style of parts of web components. That’s cool, but restricted to web components.

As a complementary solution, CSS Modules’ composition allows to reuse parts CSS classes. And, as you know, composition is more flexible than inheritance.

Functions

Sometimes you want to repeatedly perform computations without re-writing the same code.

Before

Sass allowed you to write functions. The difference with Sass mixins is that they return a value.

Today

Depending on what your function does, you may find what you need in modern standard CSS functions. Common used ones are:

  • calc() to perform mathematical computations
  • color-mix() to replace Sass functions like darken(), brighten(), etc. in a more generic way.

See the full list for more details. Also note that, should you need more complex, business oriented function, there is a good language for that: JavaScript, which can update CSS and/or its variables.

Conclusion

Sass helped a lot when most of its features where missing from browsers, but at the cost of:

  • integrating its preprocessing phase in our build pipeline. This was not always as smooth as it could be, because it required native code;
  • a generated code which was not always optimal.

Also, aside nesting, variables and mixins, a lot of its features (loops, functions, etc.) were just not used.

Today, with most of those features integrated in a standard way we can:

  • remove the cost of the preprocessing phase
  • remove the cost of using/learning a proprietary language such as Sass
  • benefit from new features which Sass cannot provide, such as dynamic variables and computations.

--

--