Do we still need CSS preprocessors in 2024?
Comparison between preprocessors and the flourishing capabilities of native CSS
Table of Contents
- Why question the necessity of preprocessors?
- The functional scope of native CSS
- So, do we still need preprocessors?
- Conclusion
Why question the necessity of preprocessors?
The introduction of CSS3 was over 10 years ago, the first release of Less almost 15 years and Sass even 18 years ago. Over this period, the CSS standard has changed a lot, and in recent years in particular, many long-awaited features have been implemented in all evergreen browsers. As a result, developers now have more options than ever to solve their problems.
Many of the killer features for which preprocessors were used in projects in the past are now part of the CSS standard and can therefore be used directly in the browser without the need for preprocessors. Variables and nesting are just two of the many additions that make CSS a much more powerful stylesheet language than it was just a few years ago, when preprocessors like Sass and Less got first introduced.
However, as of now not all features of preprocessors can simply be replaced by native solutions. Without further ado, let’s take a look at the most important features provided natively by CSS and evaluate in which cases preprocessors are still needed, if at all.
The functional scope of native CSS
Important: Some of the following functions have been available in most browsers for years, while others are still under active development and should therefore only be used with caution.
For comparisons between CSS and preprocessor solutions, Sass/SCSS is used as the preprocessor solution, as it has the largest market share according to recent surveys.
Variables
Especially in the early years of preprocessors, variables were the killer feature of all. Be it for saving colors for a theme or font sizes and font families to ensure a uniform user interface.
With CSS Custom Properties, an equivalent native solution for using variables in CSS without a preprocessor has existed for several years now. Let’s take a look at the differences between the native and preprocessor implementations:
Variables - SCSS
$colorPrimary: hotpink;
.highlight {
color: $colorPrimary;
}
Variables - CSS
:root {
--colorPrimary: hotpink;
}
.highlight {
color: var(--colorPrimary);
}
Even if there are small differences in the syntax, the two examples are still quite similar. However, especially with preprocessors, it is important to look at the output rather than the implementation itself, as this is ultimately what ends up in the browser.
The output for the SCSS code block above is as follows:
.highlight {
color: hotpink;
}
Comparing this output with the CSS implementation, the main difference becomes obvious. The SCSS variables disappear after compilation and are replaced by the value that the variable had. CSS Variables, also known as Custom Properties, behave in a different way. They remain variables and thus bring additional flexibility to the browser. The following functions, for example, can therefore be implemented in a simplified manner by using CSS Custom Properties:
- Dark- / Light-Modes
- User theming Options
- Sharing and adjusting values on the client
With preprocessors, several CSS files usually had to be built for such functionalities, as these could not be adjusted on the client side. In these cases, custom properties can also reduce the complexity of the build process and the amount of output code.
Furthermore, Custom Properties can be set and manipulated via media and container queries as well as JavaScript and thus offer a whole range of advantages over preprocessor variables.
However, there is another difference that is often overlooked. While Custom Properties follow the normal rules of order in CSS and therefore later definitions in the same context overwrite the value, SCSS variables can take on multiple values in the same context. This behavior is best illustrated by the following example:
.highlight {
$colorPrimary: hotpink;
color: $colorPrimary;
$colorPrimary: red;
background-color: $colorPrimary;
}
Compiles to:
.highlight {
color: hotpink;
background-color: red;
}
Keep in mind that this is more of an edge case and should not prevent you from switching to Custom Properties. It is just important to know if you ever run into a similar problem.
Media- and Container-Queries
Queries are a special case in connection with Custom Properties. While one would expect that var() can be used without restrictions within a media or container query, this is unfortunately not the case. Although there is already a fully specified solution with @custom-media, this is unfortunately not yet implemented in any browser.
For projects in which this is an essential requirement, PostCSS with the Custom Media Plugin offers the a good solution for using the new syntax for query variables right now.
Typing
Types are another important feature when thinking about variables in combination preprocessors. For example, SCSS supports:
- Number
- String
- Color
- Lists
- Maps
- Boolean
- null
While null and and maps are two of the things we will look at in a later section, the remaining types come straight from CSS. Sass even says in its documentation:
Sass supports a number of value types, most of which come straight from CSS.
With @property, a new CSS-Feature was introduced to configure custom properties in various ways and thus also to define a syntax that works similarly to types. Let’s take a look at such a configuration:
@property --colorPrimary {
syntax: "<color>";
inherits: false;
initial-value: hotpink;
}
Here we see several functions that this new definition brings with it:
syntax
- Which syntax must values follow?inherits
- Are set values inherited?initial-value
- Initial value when creating the custom property
While initial-value is fairly self-explanatory, the other two properties are a bit more complex, but offer enormous potential. For syntax
, for example, in addition to values such as <number>
and <color>
, lists of valid values can be passed for `syntax’. Similar to union types in TypeScript, you can define things like:
@property --colorPrimary {
syntax: "red | green | blue";
inherits: false;
initial-value: red;
}
The syntax
is used to validate the correctness of the assignment of new values. All assignments outside the permitted value range are ignored. The following example illustrates this behavior:
@property --colorPrimary {
syntax: "red | green | blue";
inherits: false;
initial-value: red;
}
.a {
background-color: var(--colorPrimary); /* red */
}
.b {
/* yellow is not allowed according to the syntax */
--colorPrimary: yellow;
background-color: var(--colorPrimary); /* red */
}
.c {
--colorPrimary: green;
background-color: var(--colorPrimary); /* green */
}
As yellow
isn’t a permitted value, the assignment is discarded and the initial-value
is used. This approach allows, for instance, the Boolean type from SCSS, which does not exist in CSS, to be mapped with "true | false"
inside of a syntax
definition. A detailed list of permitted syntax values can be found here.
The inherits
parameter determines whether assignments of the variable are passed on in the cascade. Elements that overwrite a custom property with the value "inherits: false"
therefore do not pass this newly set value on to their child elements. Instead, they fall revrt to the initial-value
. This behavior differs significantly from conventional custom properties, whose values always follow the cascade.
Nesting
Like variables, nesting of selectors, combinators and media queries has long been a feature that prompted many to use preprocessors. Nesting helps to avoid unnecessary duplication in the definition of selectors and maintain clarity. The following is a typical example of nesting:
.parent {
.child {
background-color: green;
@media (width >= 1024px) {
background-color: blue;
}
}
}
However, this is no longer a pure preprocessor feature. The previous example is not only valid SCSS, but also valid CSS that can be interpreted by browsers without an additional build step.
Even though the syntax is identical, there are fundamental differences that need to be mentioned:
- When tags are nested, unlike with other selectors, native CSS-Nesting requires an & in front of the tag name.
- In contrast to SCSS, selector lists of elements with nested selectors are resolved in CSS using
:is()
for performance reasons. This can lead to in differences in terms of specificity for complex nesting stuctures. The following example shows one of these cases:
/* Input */
.parent,
#parent {
.child {
/* ... */
}
}
/* SCSS Output */
.parent .child, /* Spezifität - 0, 2, 0 */
#parent .child /* Spezifität - 1, 1, 0 */ {
/* ... */
}
/* CSS Internal Output */
:is(.parent, #parent) .child /* Spezifität 1, 1, 0 */ {
/* ... */
}
- The possibility of concatinating selectors from several substrings, à la BEM does not exist in native CSS-Nesting. Projects that rely on naming conventions such as BEM should therefore consider sticking with preprocessors instead. At this point, however, it is worth mentioning the new CSS feature
@scope
, which can make class naming conventions such as BEM completely obsolete in the future.
Although Nesting does not open up any new development possibilities, it is nevertheless an important and noteworthy extension of the CSS standard that can keep up with the established preprocessor solutions in all respects.
Media Query Range Syntax
Although it’s just Syntactic Sugar, I think the new Media Query Range syntax deserves a place in this post.
With their help complex media and container queries can be written in a much shorter and more readable way than a few years ago. With all the evergreen browsers already supporting this new syntax, nothing stands in the way of its use in projects.
However, it is important to mention at this point that in cases where the browser does not support the new syntax, the entire style block is ignored. The feature should therefore only be used with caution, particularly in projects with high backwards compatibility requirements.
The following example shows how complex queries can be simplified:
@media (min-width: 768px) and (max-width: 991px) {
/* Old Syntax */
}
@media (768px <= width <= 991px) {
/* New Syntax */
}
Functions
For a long time another major advantage of preprocessors had been the large number of functions. Two of the most important areas of application for these were calculations and color modifications. While simple mathematical calculations have long been possible in CSS with the help of calc()
, complex calculations or changing colors had been a problem for a long time. However, a lot has happened in this area in recent years, so let’s take a closer look at the latest functions in native CSS.
Mathematical Functions
There have been many additions in the area of mathematical functions, so below is a list of all the functions available in CSS that can be used in a mathematical context.
- General Math Functions
calc()
- Basic function for calculations in CSS
- Comparision Functions
min(value, …)
- Returns the smallest of a list of valuesmax(value, …)
- Returns the largest of a list of valuesclamp(min, value, max)
- Limits a value to a specific range
- Stepped Value Functions
round(strategy, value, interval)
- Rounding functionmod(value1, value2)
- Modulo functionrem(value1, value2)
- Remainder function
- Trigonometric Functions
sin(value)
- Sine ofvalue
cos(value)
- Cosine ofvalue
tan(value)
- Tangent ofvalue
asin(value)
- Inverse sine ofvalue
acos(value)
- Inverse cosine ofvalue
atan(value)
- Inverse tangent ofvalue
atan2(value1, value2)
- Inverse tangent ofvalue
andvalue2
- Exponential Functions
pow(value1, value2)
- Exponentiatesvalue1
with the exponentvalue2
sqrt(value)
- Square roothypot(value, …)
- Returns the square root of the sum of the squares of its argumentslog(value1, value2?)
- Logarithm of the numbervalue1
to the base ofvalue2
exp(value)
- Exponentiated e with the exponentvalue
- Sign-Related Functions
abs(value)
- Returns the absolute value ofvalue
sign(value)
- Returns -1 ifvalue
is negative and 1 ifvalue
is positive
In addition to functions, there are now various mathematical constants that can be used in calculations:
e
- Euler’s numberpi
- Piinfinity
/-infinity
- Infinity / negative InfinityNaN
- Not a Number
Color Functions
Similar to the area of mathematical functions, a lot has also happened in the area of color functions in recent years. Today, colors can be specified not only in the familiar hexadecimal, RGB and HSL formats, but also in a variety of other formats that offer various advantages. These formats include:
hwb()
lab()
lch()
oklab()
oklch()
If you want to learn more about the different formats and the corresponding color spaces, I can recommend this article by Evil Martians.
Since mid-2023, we also have the color-mix() function at our disposal, which can be used to mix colors and output the result in a predefined color space. Here’s an example:
:root {
/* Mix white and black in the sRGB color space in the same ratio */
--color1: color-mix(in srgb, white, black);
/*
* Mix red with 50% transparency and green in the sRGB color space in the ratio 25% / 75%
*/
--color2: color-mix(in srgb, rgb(255 0 0 / 0.5) 25%, rgb(0 255 0));
}
Meanwhile all evergreen browsers besides Firefox have also implemented the new so called Relative Color Syntax. This syntax enables the creation of new colors relative to an original color and thus facilitates the mapping of preprocessor functions such as darken, lighten, transparentize or saturate as well as many other color functions in CSS.
It is important to understand the syntax, as this can be somewhat confusing at first glance. The following illustration shows and describes the individual parts of such a definition.
Below is an example of how this function can be used to manipulate colors in various ways:
:root {
/* Create new color in Hex */
--primary: #ff0000;
/* Convert color to oklab */
--primary-as-oklab: oklab(from var(--primary) l a b);
/* Make the color 10% lighter */
--primary-lighten-10: oklch(from var(--primary) calc(l * 1.1) c h);
/* Make the color 10% darker */
--primary-darken-10: oklch(from var(--primary) calc(l * 0.9) c h);
/* Set brightness of color from 70% */
--primary-70: oklch(from var(--primary) 70% c h);
/* Set the transparency of the color to 50%*/
--primary-transparency-50: rgb(from var(--primary) r g b / 50%);
/* Complementary color */
--primary-complement: hsl(from var(--primary) calc(h + 180) s l);
}
As can be seen, the Relative Color Syntax enables a variety of color manipulations. The examples shown above only give a small insight into what is actually possible. The new syntax can, for example, help with the creation of an entire color palette with just one source color and reduce the maintenance effort for complex applications with multiple themes.
If-Statements
Although the main feature that allows you to write If-like conditions in CSS is still under development, it is important to show what features will be available to us in the near future.
I deliberately call it If-like, as it has little to do with known if/else conditions, but enables the same.
The feature are the so-called Style Querie. It is a sub-feature of container queries that can be used to set a set of styles depending on Custom Property values or other styles. Below is an example:
.parent {
container-type: style;
}
.child {
/* Container Style Query depending on the background color of the container */
@container style(background: #000) {
.black {
color: #fff;
}
}
/* Container Style Query depending on the value of a custom property */
@container tile style(--round: true) {
.round {
border-radius: 50%;
}
}
}
As can be seen from the example, Style Queries use a similar syntax to Container Queries. The only new feature is the style() function, which tells the browser to interpret and evaluate the following parameters as styles.
This new syntax lets us bring a lot of the styling logic that’s often been outsourced to JavaScript back into CSS. In the future, we won’t have to toggle multiple classes in different places or pass styling properties to components anymore. Instead, we’ll be able to handle all those different states directly inside of the CSS and toggle them via Custom Properties. Especially in conjunction with the @property
syntax mentioned in the variables section, this provides developers with an incredibly powerful new tool.
So, do we still need preprocessors?
After taking a look at the many new features & functions in CSS, the question rightly arises: Do we still need preprocessors at all? Because, as we have seen up to this point, the majority of the functions that were available exclusively in preprocessors in the past are now available natively in CSS.
However, there are some functions that cannot currently be replaced by CSS. We will look at the most important of these below.
Selector Compounding (BEM)
As already mentioned in the Nesting section, the current version of native nesting does not have the option of creating selectors from several individual strings. For various reasons, including the complexity of the implementation, there are currently no ambitions to add this at a later date.
Stylesheet Concatenation
It is not possible to merge multiple CSS files without a build step. The alternative solution using native @import is generally considered a bad practice due to the performance impact.
Maps, Iterators and null
Data types such as maps, arrays or null and the associated auxiliary functions are not included in the CSS standard.
Mixins
In recent years, the use of mixins has decreased and it is becoming increasingly popular to provide styles in the form of utility classes to share them between components. Although the approach is different, it achieves the same result. If you still want to use mixins, you still need a suitable preprocessor.
Functions
While it may not be feasible to write your own functions à la Sass & Co. in CSS, it is possible that some of the recent additions could replace a significant portion of the logic previously handled by self-implemented functions. Therefore, the decision to use a preprocessor depends on the specific needs of the project.
Conclusion
In the current landscape, many projects no longer require preprocessors, as many of the distinctive features that made Sass, Less & Co. so popular have long been incorporated into the CSS standard and, in some cases, offer significantly more functionality than their preprocessor counterparts. Nevertheless, they continue to have their place. However, there are only a few arguments in favor of moving a completely finished and functioning codebase entirely to native CSS.
For new projects, the new features can be an incentive to switch to a setup without a preprocessor in order to familiarize yourself with the native new functions. If necessary, they can still be used in combination with preprocessors. This is one of the great advantages of CSS: in most cases, new functionalities are compatible with preprocessors and can therefore be used independently of the project and setup.