Should you use Atomic CSS in your design system implementation?

Boris
Thinkific
Published in
8 min readSep 28, 2023

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/: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.

I would love to see more discussion on the topic, so please share your experience on the matter. Do you have questions about your special usecase? Are there any points (whether it’s benefits or drawbacks) that I missed? Do you want to share an experience of using Atomic CSS for design system implementation?

Let's have a discussion!

Want to learn more about #TeamThinkific? Check out Thinkific’s Career Page and join our Talent Community to learn more about what our team is put to, new job opportunities, company news, and more!

Stay connected with #TeamThinkific on Facebook, Twitter, Instagram, and LinkedIn!

--

--