A common mistake among developers is to give less importance (often, none at all) to styling when compared to other parts of their codebase.
Since the early years of CSS, there is a very popular practice of accumulating everything in a single stylesheet, carelessly, quickly creating a huge maintenance nightmare.
It’s a very simple philosophy:
styles.css
!important
will do the job. As a precaution, you can use it for everything, so it will work for surebody>header#main_header.main-header>img.logo+div.links>a.link
Fortunately, in recent years, this way of thinking has become more and more obsolete.
A lot of things have changed in the web: in the browsers, in their three main languages, in the explosive growth of the number of available libraries and frameworks, in the emergence of many incredible tools to facilitate our everyday work, and, last but not least, in the front-end community and its growing thirst for self-improvement.
The evolution of CSS is continuous. Features like CSS Grid Layout, Flexbox, custom properties, writing-mode, and others are quickly appearing and being supported. Even so, maintaining the appearance of a complex web application with only vanilla CSS is a thankless job, regardless of whether you are following the best practices or not.
The fact is that any project, minus a few exceptions, can be favored by pre- and post-processing tools or CSS-in-JS libraries. In this article, we will see both how they work and their advantages.
A CSS preprocessor is a tool that transforms code in a given language into pure CSS. The most popular ones are Sass, Stylus, and Less. Among them, there are differences in the feature set and the language syntax.
The purpose of this article is not to make a detailed comparison between the available options (since there are many great posts on the internet with this goal), but to introduce preprocessors in a general way. For the examples, I chose Stylus, a tool written in JavaScript, which runs on Node.js.
Take a look at this piece of code:
$base-color = #2DD; $base-color--dark = darken($base-color, 30%); .menu { background-color: $base-color--dark; &::before { color: $base-color; } }
At the beginning of the code shown above, I declared two variables, whose values are colors. The first one has a literal value. The second one uses a native Stylus function, darken
, to make the color stored in the $base-color
variable 30 percent darker.
Then, I declare a class named menu
. In the body of this rule, I use both variables that have been declared previously. Finally, there is an occurrence of selector nesting, where I set the color of the before
pseudo-element for the menu
class.
Note: Using the dollar sign as a prefix for variable names is a question of personal preference and not a Stylus demand. I acquired this habit over the years working with Sass, and I still use it these days as a best practice in Stylus. This way, the variables become more visible, not being confused with CSS keywords.
The example above has features that are not supported by the browser, e.g., selector nesting and variables (OK, now CSS has custom properties, as I explain in this article, but their usage and syntax are different and somewhat peculiar). A compilation process is necessary in order for the browser to correctly interpret Sass, Stylus, or Less code. After compilation, the output is just plain CSS:
.menu { background-color: #189b9b; } .menu::before { color: #2dd; }
As you can see, the final result expands nested selectors as separate rules and replaces each variable reference with its respective value. Just like that.
The supported features vary between preprocessors. Among the main ones, Sass and Stylus are the most versatile and complete. Once again, I’ll use Stylus to explain each concept.
A variable can store things like:
$base-color = blue;
$cta-color = #D32;
$link-color = rgb(15, 200, 25);
$container-padding = 2rem;
$rotation-increment = 15deg;
$default-transition-duration = 20ms;
$sequence = 1, 2, 3;
$colors = blue, #F00, hsla(120,100%,30%,0.5);
$font-stack: “Helvetica Neue”, Helvetica, Arial, sans-serif;
It’s possible to nest selectors in order to avoid code repetition and to group correlated rules in a clearer way:
.main-navbar { ul { list-style-type: none; } a { color: #D22; &::after { margin-left: 1rem; } &:hover{ opacity: 0.8; } } }
Advice: It’s important to point out that this feature needs to be considered carefully, since it is prone to problems if overused, such as: stylesheets highly coupled with the HTML hierarchical structure, selectors with high specificity, and an unnecessarily large CSS output.
Like OO languages, preprocessors allow code reuse through inheritance.
A class X can extend another class Y, inheriting all of its properties, and have properties of its own:
.btn { border-radius: 0.3rem; border: 0.1rem solid #222; &:hover { opacity: 0.9; } } .btn--blue { @extend .btn; background-color: #22D; }
Besides single inheritance, exemplified above, it’s possible to inherit styles from multiple classes at the same time:
.btn { border-radius: 0.3rem; border: 0.1rem solid #222; &:hover { opacity: 0.9; } } .big-btn { font-size: 1.8rem; padding: 1rem 2rem; } .big-btn--blue { @extend .btn, .big-btn; background-color: #22D; }
And to continue with our comparison with OO languages, let’s introduce the concept of the placeholder, originally popularized by Sass, which can be compared to an abstract class in OOP. A placeholder is a special type of class that can only be extended. Which means you can’t reference it directly from your HTML, once it won’t be present in your final CSS output. The code in a placeholder will only be rendered in case a class extends it.
We extend placeholders in the same way we use common CSS classes:
$btn { border-radius: 0.3rem; border: 0.1rem solid #222; &:hover { opacity: 0.9; } } .btn--blue { @extend $btn; background-color: #22D; }
In Stylus, placeholders are prefixed with a dollar sign. In the above code, the .btn--blue
class will inherit the entire body of the $btn
placeholder, but it, in its turn, won’t be present in the compiled CSS.
Like any structured language, it’s also possible to use conditionals and loops:
$colors = 'blue', 'black', 'yellow', 'aqua'; $dark-colors = 'blue', 'black'; for $color in $colors { .btn--{$color} { background-color: unquote($color); if $color in $dark-colors { color: white; } else { color: #222; } } }
Here’s the compiled CSS:
.btn--blue { background-color: blue; color: #fff; } .btn--black { background-color: black; color: #fff; } .btn--yellow { background-color: yellow; color: #222; } .btn--aqua { background-color: aqua; color: #222; }
Functions are included in the main preprocessors as well. A function behaves exactly the way you’re used to from other languages, receiving from 0 to n arguments and returning a value:
repeat($str, $qty) { if ($qty < 1) { return ''; } $ret = ''; for $i in 1..$qty { $ret += $str; } $ret; } h1:before{ content: repeat(ha, 5); }
CSS output:
h1:before { content: 'hahahahaha'; }
Mixins are very similar to functions, but with the following differences:
square($side) { width: $side; height: $side; } .card{ square(20rem); background-color: #3C3; }
CSS output:
.card { width: 20rem; height: 20rem; background-color: #3c3; }
You probably already know the CSS @import
at-rule, which allows a stylesheet X to include an external stylesheet Y. This is made by the browser during the interpretation of the stylesheet, triggering an additional HTTP request.
Preprocessors save the browser from having to make these additional requests for each included CSS. When you import a Sass, Stylus, or Less file, the included stylesheet and the one that includes it become one single CSS bundle (after compilation) to be loaded in your application.
To make it clearer, here is a simple example:
// color-scheme.styl $base-color = #555; $cta-color = #D22;
// another-thing.styl .whatever { padding: 2rem; background-color: $cta-color; }
// example.styl @import 'color-scheme'; @import 'another-thing'; .footer { background-color: $base-color; color: white; }
The main file, example.styl
, imports two other files. In the first one, we can see two variables being defined, which will become globally accessible from this moment on. The second file has a CSS rule defined, which uses one of the variables we defined previously. After compilation, here’s what the CSS output will look like:
.whatever { padding: 2rem; background-color: #d22; } .footer { background-color: #555; color: #fff; }
Naturally, in the real world, the final bundle would be minified and gzipped before being served to the user.
Among the many possible uses, native functions help us when dealing with:
push
, pop
, index
, shift
, keys
)typeof
, unit
)round
, sin
, floor
, abs
)split
, substr
, slice
)Native functions can be used both to compose your mixins/custom functions logic and directly in the body of a rule.
In theory, a post-processor is a tool whose input is a CSS file and whose output is a transformed CSS file. Note that, differently from preprocessors, a post-processor doesn’t involve another language; it’s just plain CSS.
So, what kind of transformation are we talking about? Well, many kinds. Here are some good examples of post-processing plugins:
All of the above examples are PostCSS plugins. Many regard PostCSS a top-of-mind name when it comes to post-processors.
However, there is some discussion in the development community about the meaning of the term “post-processor.” Some argue that the “post” prefix would only make sense if the processing took place in the browser (like -prefix-free, for example) and not before, in development time.
Others disagree with the premise of “valid CSS in, valid CSS out” since many post-processing plugins use nonexistent properties or invalid CSS syntax to make their magic happen. Some plugins even mimic the typical preprocessor behavior, supporting mixins, inheritance, loops, conditionals, and other features not natively supported by CSS.
Here are some examples of post-processing plugins that use CSS non-standard syntax and properties:
calc()
CSS function. Depends on the use of non-standard properties (e.g., lost-column
, lost-row
, lost-utility
and lost-center
)After the massive adoption of PostCSS by developers and hundreds of plugins created for a wide variety of purposes, the project maintainers themselves realized that the term “post-processor” no longer makes sense and officially stopped using it.
Independently of the semantic debates about name of the concept, the potential of a tool like PostCSS is unquestionable, and due to its great flexibility, it can be used both with a preprocessor (like Sass, Stylus, or Less) and as a replacement for it (through plugins like PreCSS, for example).
The React explosion has popularized a number of concepts among front-end developers, like functional programming, reactive programming, Flux architecture (and its most famous incarnation, Redux), and others.
One of the most recent trends born in the React world is CSS-in-JS, the idea of writing styling code in JavaScript, rendering the resulting CSS in runtime. The most popular library in this category is, certainly, styled-components, but there are other relevant options, like JSS, Radium, Emotion, and Aphrodite, just to list a few.
Christopher “Vjeux” Chedeau, a front-end engineer at Facebook, is often credited as the precursor of CSS-in-JS. He got the idea while working on the React team in 2014, as you can see in this presentation.
When Vjeux created CSS-in-JS, its original goal was to solve seven CSS problems:
I’ll use styled-components to exemplify the concept:
import React, { Component } from 'react'; import styled from 'styled-components'; const Title = styled.h1` color: white; font-size: 2rem; `; const Content = styled.div` background: blue; padding: 2rem; `; class Example extends Component { render() { return ( ); } } export default Example;
For those who have some knowledge of React, the above example speaks for itself. The constants Title
and Content
are normal React components, but with a custom style.
The styled.div
notation is an ES2015 native feature, and it’s known as Tagged Template Literal. In this context, it allows us to dynamically handle the style of a component based on its properties, like in the example below:
const SignInButton = styled.button` font-size: ${ props => props.standout ? '2rem' : '1.4rem' }; `;
While it is true that libraries such as styled-components are widely recognized and used by the community, there is a considerable mistrust, or even rejection, of the CSS-in-JS concept by a large number of developers, with many questioning its original goals. There are many reasons for this, but the most obvious one is the discomfort brought about by the idea of defining style in a JavaScript file.
The fact is that some of the problems that CSS-in-JS aims to solve already have other solutions nowadays. CSS Modules are a good example, allowing you to modularize stylesheets in an elegant way, preventing conflicts between global identifiers.
Furthermore, they can also be combined with a preprocessor. The consistent use of a naming convention, such as BEM, also prevents these problems. CSS custom properties, which already have a decent browser support, let you share values between CSS and JavaScript. And so on…
There are many modern alternatives to plain CSS. There is no perfect and universal solution among them, each one with its own pros and cons. It’s up to the developer to choose the best fit for each project.
What matters is not to be bound by the limitations of vanilla CSS in projects of relevant size or with a reasonable growth perspective, suffering to maintain your stylesheets for not knowing better available options.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]