Table of Contents
According to Betteridge's Law of headlines, the answer should be "No".
But is it that simple? Why use Atomic CSS where CSS-in-JS can be used? What special benefits does Atomic CSS have in the Design System context? Is there a price to pay for these benefits?
Continue reading and you will get a fresh perspective on these questions.
Design Systems are nothing new — some authors trace their origin back to the pre-Internet era. But even in the context of the Internet: Design Systems were a thing 10 years ago when I started my career as a web developer. Fast-forward 10 years — and I finally have an opportunity to work on implementing a Design System at Thinkific. I went through a lot of different content (articles, books and workshops) on building Design Systems and tend to think that I have a pretty good overview of what's usually discussed in the Design System communities.
Atomic CSS
is nothing new either. The first time I heard about it was about the
same time that I heard of Design Systems (i.e. 10 years ago). Back then
I was kind of puzzled: What benefits do single-property class names
bring to the table? It felt "hacky" to have 5–10 different items in the
class
attribute of most of the HTML elements and I wasn't entirely
sure what would be the proper use-case for this approach.
Given how old these ideas are I'm surprised that I haven't seen much content discussing the synergy between them. I wonder if "implementing a Design System" is the "proper use-case" for the Atomic CSS that I was looking for earlier in my career. Hopefully, this article will advance the discussion somehow.
Why Atomic CSS
Individual "atomic classes" from Atomic CSS seem to be fairly similar to the concept of "design tokens". And by their very nature design tokens are used extensively across the whole Design System (that's basically what the "System" part of the term means). Having a single CSS class for a token means that we can save a ton of space by re-using that class across components (compared to duplicating the tokenized property in individual component classes).
Here is a list of other reasons that made us try Atomic CSS in our Design System implementation:
Plays well with design tokens
Individual "atomic classes" from Atomic CSS seem to be fairly similar to the concept of "design tokens". Both are restricted sets of values that are used extensively across the codebase for styling purposes. Each atomic class can be matched with a single design token value.
Smaller stylesheets
Having a single CSS class for each tokenized value means that we can save a ton of space by re-using that class across components (compared to having to duplicate the property declaration with the tokenized value in individual component classes).
Scalability
Adding new components should not have any additional impact on the size of a singleton stylesheet, because we don't have to define any new classes (given that our component uses already existing design tokens).
Maintenance benefits
Having a dedicated utility for each styling need means that we're able to adjust styling behaviour on a per-use case basis (see an example provided in the "The case for Atomic CSS" section below).
Tooling
Besides, our selected styling solution (Vanilla Extract) provided an integrated way to generate atomic classes (a Sprinkles framework). This allowed us to leverage the power of TypeScript to ensure correct token names are used and also enjoy the TypeScript autocompletion when authoring the styles.
The case for Atomic CSS
One particular case proved Atomic CSS to be even more useful than we initially thought.
At some point during the implementation, our designer noticed that the
implemented components did not use the spacing tokens in the same way as
they were used in Figma. The spacing system in Figma tries to keep the
spacing consistent regardless of the component border width, which means
that the border "overlaps" the padding (e.g. without a border an element
has 8px
padding, but with the 2px
border applied we are left with
only 6px
padding). Whereas our components were simply using the
padding
and border
CSS properties (and none of the existing
CSS
box models could help us ensure that border and padding add up to a
"design token" value).
Without the Atomic CSS, we would have to go to each of the relevant
component classes and manually adjust the value of padding
to account
for the border
applied (which might be challenging if the component
has multiple variants with different border
widths).
With Atomic CSS we have individual classes generated for all possible
values of the padding-*
and border-width-*
:
/* Vanilla Extract actually produces hashed class names like `._194zije15`, * but let's use something more readable instead */ .pt-a { padding-top: 8px } /* … same for right/bottom/left paddings */ .pt-b { padding-top: 12px } /* … same for right/bottom/left paddings */ /* … same for other padding-related spacing tokens (e.g. 16px, 24px etc.) */ .btw-a { border-top-width: 1px; } /* … same for right/bottom/left border widths */ .btw-b { border-top-width: 2px; } /* … same for right/bottom/left border widths */ /* … same for other border-related spacing tokens (e.g. 3px, 4px, etc.) */ /* The `padding` and `border` shorthands are translated by Sprinkles into the combinations of the individual classes above */
These classes are used in every component, therefore we only needed to
find a way to tweak the values used in padding classes to depend on the
set of border classes applied to the same HTML element. Given that the
border values are only set by our atomic classes, we can obtain the
border value applied by saving it into the
CSS
custom property. Then we can adjust the padding value (by subtracting
previously obtained border value from it) using a CSS calc
function.
Here's what we ended up with:
/* Vanilla Extract actually produces hashed custom properties like `--_194zijea2`, * but let's use something more readable instead */ .pt-a { padding-top: calc(8px - var(--btw-value)); } /* … same for right/bottom/left paddings */ .pt-b { padding-top: calc(12px - var(--btw-value)); } /* … same for right/bottom/left paddings */ /* … same for other padding-related spacing tokens (e.g. 16px, 24px etc.) */ .btw-a { --btw-value: 1px; border-top-width: var(--btw-value); } /* … same for right/bottom/left border widths */ .btw-b { --btw-value: 2px; border-top-width: var(--btw-value); } /* … same for right/bottom/left border widths */ /* … same for other border-related spacing tokens (e.g. 3px, 4px, etc.) */
In practice, this change was just a minor configuration tweak in Sprinkles. After it was done the spacing in all of the implemented components started to behave just like it does in Figma. And every new component implemented with our Atomic CSS utilities is going to behave the same (whereas without Atomic CSS we would have to remember to implement the correct spacing behavior for each new component). Neat, right?
The tradeoffs
There is no such thing as a free lunch, and using an Atomic CSS in your Design System implementation comes at a cost. In this section I'll try to enumerate all of the possible drawbacks:
Specificity issues
With Atomic CSS your selectors have the same
CSS
specificity (0-1-0
), so whichever one wins the conflict depends on
the source order. There are two problems with that:
- The source order is usually controlled by the tooling that generates the Atomic CSS
- Multiple conflicts might require different source order resolutions that conflict with each other
This requires you to avoid any conflicts when applying your utility
classes, which can become very tricky in some cases. E.g. you need to
reset a margin on an element, but you can't use an atomic class for that
because this margin can be redefined by another component variant (or by
the user); this makes you use an element selector for that, but this
might trigger unwanted changes in the unrelated element in consuming
application, so you have to be more precise and use something like
hr:where(.your-atomic-class)
to keep the element selector specificity (0-0-1
) but scope your
selector to the elements with your-atomic-class
applied.
Conditional styles
This is a known weak spot of Atomic CSS: if you need to use different
property values based on some condition (e.g. :hover
or :focus
or any
other pseudoclass, media query or container query) - you'll need to
generate a separate class for that. If you need a lot of values at a
multitude of conditions the size of your stylesheet might balloon
uncontrollably. You can try to use JS to detect the condition and apply
the proper classes for you, but that might have performance
implications.
That said, the exact performance impact depends on the amount of values and conditions required by your Design System. It's better to measure the impact (by stubbing additional conditions and values) before jumping to any conclusions. E.g. for our design system the measurements suggest that we would have to add 15 more conditions (and/or values) to exhaust our performance budget (and we're unlikely to add that much).
Implied structure
The Atomic CSS implies that a restricted set of styles gets reused over and over again in different contexts. You might not see any performance/maintenance benefits if your Design System isn't based on design tokens or doesn't follow a restricted set of values (like a palette of colours, set of spacing values, etc.).
Scaling difficulties
By its nature, Atomic CSS is used across any boundaries that you can draw within your Design System (be it component groups or individual components). Therefore if your Design System ever grows so big that you want to split it — the Atomic CSS becomes a shared part that might prevent you from splitting it effectively.
Tooling complexity
Setting up (and having to maintain) the tooling to generate Atomic CSS can be an unjustifiable burden for some people. It can also become a barrier if someone new is trying to contribute to your Design System.
Conclusion
My answer to the question in title is "it depends", of course. Using Atomic CSS for the Design System implementation has its benefits, but it also comes with a number of drawbacks.
You have to clarify the requirements for your Design System to understand whether you can manage the drawbacks of Atomic CSS in order to enjoy it's benefits.