Atomic CSS Modules
A few months ago we had the opportunity to create a new product from scratch at YPlan and we wanted to do it in the right way. Our aim was to craft beautiful UI components which we could easily extend and reuse across the application.
Atomic CSS is a method of writing CSS where a class defines a single property. The result is a CSS file in which every declaration appears only once.
Our first reaction was “nope” but we have seen a lot of smart people being excited about it and we decided to give it five minutes.
The biggest problem of Atomic CSS is that every single element has many (many!) classes, which makes the code unpredictable and hard to read.
Conceptually, Atomic CSS is similar to Inline Styles because every single rule is declared on the element itself but there are some substantial differences: first of all, Atomic CSS is real CSS and therefore cacheable. Second, Inline Styles don’t support pseudo-classes and media queries while Atomic CSS does.
Applications created with Atomic CSS are usually hard to maintain because the styling logic is within the HTML rather than being within the Stylesheets. So, whenever the style has to be changed, instead of updating the CSS, you end up editing the markup and that is wrong.
Obviously, Atomic CSS has some benefits too with the first one being that it allows quick prototyping. Once the base rules are done, it is incredibly fast to set up and style new pages, which is pretty important. Especially for startups where the creation of UIs is an iterative process, like at YPlan.
The second benefit of Atomic CSS is that every time you build a new component you can just compose the base classes, instead of creating new rules. Consequently, the size of the CSS file doesn’t grow linearly with the number of components and that is great for performance.
We believe CSS Modules is the biggest improvement made to CSS in the last few years. The concept behind it is so smart and simple that we fell in love immediately.
Gets compiled to:
As you can see, using CSS Modules is pretty straightforward and there are several benefits. You avoid polluting the global scope, which is one of the major issues with CSS and it is bad by definition. They also prevent the class names from clashing, which represents a common problem in large codebases and something that developers have tried to solve in the last few years by prefixing class names using different patterns. BEM has been the more popular choice.
In our opinion, the coolest feature of CSS Modules is that it allows you to declare dependencies explicitly and define the CSS required for each component in a clear way.
CSS Modules also provides new features to CSS with the most interesting one being `composes`. Composition is all about reusing classes and styles. It is conceptually similar to SASS’ extend but it doesn’t bring all the downsides of it. Whenever a class composes another class, CSS Modules doesn’t change the CSS rules but applies both classes to the element instead.
Gets compiled to:
Is it obviously possible to compose more than one class per rule and also compose classes from external files, like this:
Integrating CSS Modules in a project is very easy, especially if you are already using Webpack in your setup: you just have to add a new loader for the CSS files.
Since the class names are locally scoped, you have the opportunity to choose the algorithm used for compiling them and you can, for example, decide to use emojis as class names, which is pretty awesome.
We decided to solve the main Atomic CSS-related problems using the power of CSS Modules, getting all the benefits of both solutions.
For example, instead of manually adding multiple classes to our elements, we use CSS Modules’ `composes` to combine the base rules inside our Stylesheets, which allows us to keep the styling where it belongs.
The first step was to sit down with our designer, Karolis, to go through the designs and create a consistent set of rules for every aspect of the designs.
We decided on typography scale, grid, gutters, colours and the whole shebang that you probably know.
After we agreed on most of the rules, it was time to decide on a naming convention, to keep everything consistent and predictable.
For most rules where we have different scales (margins, paddings, font-sizes, etc…) we decided to use the following convention:
We also have more scale variations, where we use numbers to indicate percentage:
After a few iterations and changes, we felt it was time to move to the fun bit: creating components.
This part went extremely smooth and fast. As you can see in the example above, we only composed from the set of rules we had already created. Because of this our CSS didn’t change at all!
There are still some open questions around Atomic CSS Modules and one of those is related to media queries. We found a solution we are happy with, using postcss-import to simplify the creation of the rules for the different queries but it still requires more classes to be added to the element and that is not optimal.
A similar problem is related to pseudo-classes: in fact, `composes` only works with classes and the solution suggested by the CSS Modules team requires the creation of extra classes. We like consistency so we created a PostCSS plugin to make CSS Modules’ `composes` work with any selector. Unfortunately our plugin works in a similar way to SASS’ mixins, as it replaces the composed declarations with the actual CSS rules, which obviously increases the size of the CSS file going against one of the main reasons we adopted Atomic CSS.
Last but not least, in our build process we use the Extract Text Plugin for Webpack to export the CSS file separately from the bundle so that we can put it on a CDN with all of the advantages (caching, etc.). The problem is that the plugin tries to keep consistency on the order of the imported dependencies, which is something that doesn’t work for CSS. Especially if you compose multiple files in different CSS rules with various orders. The order doesn’t matter in our use case but the plugin throws an error so we opened a PR (please give us a +1).
We are very happy with our solution even if it took some time to figure out how to compose all of the pieces of the puzzle.
The biggest wins so far have been:
- no more global selectors
- explicit dependency declaration
- quick prototyping
- a non-growing CSS file
We are constantly monitoring our CSS bundle to check if everything is working as expected.
In the next few weeks, we will release our Style Guide and open source some code, so stay tuned!
Image Credit: Austin Smart (https://unsplash.com/photos/9eVgPVo2uxE)