My Approach to Atomic CSS

I’ve been having to do some web work in between game development tasks, and I decided to take a look at some of the emerging patterns and trends, namely TailwindCSS (I also tried Backbreeze).

Tailwind is an atomic CSS framework. The goal is to allow greater composition of styles through small utility classes, rather than larger, more structured groupings of rules created for each type of element or component on a page using standards like BEM.

After trying it, I definitely see the benefits of integrating more utility classes into my projects. I also thought their approach to handling media queries and pseudo classes was fairly clever. However, at the risk of sounding like a curmudgeonly old dev, I ultimately wasn’t sold on adopting a framework like it wholesale.

My reasons probably aren’t too disimilar of most folks who are reluctant to adopt it:

  1. The CSS-in-HTML approach1 was actually annoying to write and maintain, even with a component system. Not to mention trying to read things from a glance essentially meant reading all the CSS rules involved2.
  2. It purposely tries to avoid cascading changes, which is both a blessing and a curse. Having the ability to make cascading changes can sometimes even be desirable.
  3. While the CSS optimizations (PurgeCSS) did help my stylesheet filesizes remain small, the size of my HTML was quite a bit larger as a result due to the number of classes needed.
  4. Defining my CSS classes and their rules using JS felt somewhat bizarre and like a separation of concerns3 violation.
  5. The units used in class names, and the related rules, weren’t well described (at least in the parts of the documentation I was reading).

But as I said before, I do see some benefits of adding atomics over a pure component approach. In fact, using an atomic CSS framework has led me to add some atomics of my own. I sort these into 2 categories…

1) Single-Rule Atomics

Heads up! Upon further usage of this approach, I’ve actually started using more compound classes with the atomic components approach (keep reading). This means I’ve come to rely on single-rule atomics less and I’m finding it to be a far better experience overall.

As the name implies, these are similar to Tailwind in that they are classes that expose a singular CSS rule. But! Rather than atomics just being single CSS rules as classes, I wanted my atomics to support whatever design system/rules that I’m working with.

For example, anything to do with common sizing using the convention of 2xl, xl, sm, md, lg, xl, and 2xl with md being the “default”. The units assigned to these values would be directly based on whatever design spec I’m using. The ability to continue increasing or decreasing in size (3xl, 4xl, 5xl) makes it easy to start from the base font size and go either direction.

I find that atomics that affect sizing/spacing are especially useful, because I use relative units (namely em) to handle the bulk of the sizing for components and helper classes.

// size rules effect font-size and start from root size
.size\:xs { font-size: 0.75rem; }
.size\:sm { font-size: 0.875rem; }
.size\:md { font-size: 1rem; }
.size\:lg { font-size: 1.125rem; }
.size\:xl { font-size: 1.25rem; }
.size\:2xl { font-size: 1.5rem; }

// for longer properties like padding-top, I use shorthands like pt
.pt\:none { padding-top: 0; }
.pt\:xs { padding-top: 0.5em; }
.pt\:sm { padding-top: 1em; }
.pt\:md { padding-top: 2.5em; }
.pt\:lg { padding-top: 4em; }
.pt\:xl { padding-top: 6em; }

Though I actually still use SCSS quite a bit, so in actuality this code looks more like:

$spacing-sizes: ("none": 0, "xs": 0.5em, "sm": 1em, "md": 2.5em, "lg": 4em, "xl": 6em);

@each $k, $v in $spacing-sizes {
    .padding\:#{$k} { padding: $v; }
    .pt\:#{$k} { padding-top: $v; }
    .pb\:#{$k} { padding-bottom: $v; }
    .pl\:#{$k} { padding-left: $v; }
    .pr\:#{$k} { padding-right: $v; }

    .margin\:#{$k} { margin: $v; }
    .mt\:#{$k} { margin-top: $v; }
    .mb\:#{$k} { margin-bottom: $v; }
    .ml\:#{$k} { margin-left: $v; }
    .mr\:#{$k} { margin-right: $v; }

    // these actually break the single-rule rule but are very useful
    .mx\:#{$k} { margin-left: $v; margin-right: $v; }
    .my\:#{$k} { margin-top: $v; margin-bottom: $v; }
    .px\:#{$k} { padding-left: $v; padding-right: $v; }
    .py\:#{$k} { padding-top: $v; padding-bottom: $v; }

}

I also didn’t wind up using the pseudo class rules like :hover or :focus at all - opting instead to handle those on the related component or element classes like .button or .input. And the only single-rule, media query atomic(s) I found myself using frequently in Tailwind was none md:block for hiding content on smaller devices. Though even this was handled better using a pre-existing .nomobile helper that I had.

@media screen and (max-width: 767px) {
    .nomobile { display: none !important; }
}

Closing up the discussion around single rule atomics, other than sizing and spacing, the only other area where I found atomics super useful were text elements and flexbox. Even still I used an extremely sparse handful of them, meaning most could have been omitted entirely.

.text\:light { font-weight: light; }
.text\:bold { font-weight: bold; }
.text\:italic { font-style: italic; }
.text\:uppercase { text-transform: uppercase; }
.text\:lowercase { text-transform: lowercase; }
.text\:left { text-align: left; }
.text\:center { text-align: center; }
.text\:right { text-align: right; }
// and so on...

.flex { display: flex; }
.flex\:wrap { flex-wrap: wrap; }

// yes, this does break the single-rule rule a bit too
.flex\:row { display: flex; flex-direction: row; }

.justify\:center { justify-content: center; }
.justify\:between { justify-content: space-between; }
.justify\:evenly { justify-content: space-evenly; }
.justify\:start { justify-content: flex-start; }
.justify\:end { justify-content: flex-end; }
// and so on...

2) Atomic Components

Moving away from single rule atomics, let’s dive into this buzzword I probably coined. These can often be found in larger frameworks like Bootstrap and Bulma as helpers or utilities and I found these to be far more useful than their single-rule counterparts.

In fact, I’ve started trying to turn to these first rather than relying on a component-oriented architecture4. The goal is to provide small components that do most of what is needed for common or generic use cases, but can be easily be modified by the single rule atomics.

For example, if I need a “content bar” I have an atomic component called hbox, which stands for…. you guessed it… “horizontal box”. This is just a flex box that starts as stacked set of items that will automatically unstack into a horizontal bar as the screen gets wider. If I was using Tailwind as intended this would wind up being flex flex-col gap-10 md:flex-row md:items-center.

.hbox {
    display: flex;
    flex-direction: column;
    gap: 2.5em; // same as gap:md
}

@media screen and (min-width: 768px) {
    .hbox {
        flex-direction: row;
        align-items: center;
    }
}

And again, these are intended to be used in coordination with single-rule atomics. So a navigation bar with items that stack on mobile but that expand to a bar with 2 areas on either side can easily be expressed as…

<nav class="hbox justify:between">
    <div>Brand</div>
    <div>Nav Items</div>
</nav>

Closing Thoughts

If Tailwind works great for you and your team, then great. But if not, I think there’s still some value in adopting a more utility-first mindset when creating stylesheets for your apps or websites.


  1. It reminded me of the time before CSS, when everything was table elements and I my style code was HTML attributes like <td bgcolor="#000">. ↩︎

  2. Some people might see this as a benefit, but it definitely slowed me down. ↩︎

  3. From Wikipedia: In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections such that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program. ↩︎

  4. Funny enough, I think this ultimately aligns with Tailwind’s own goal and philosophy of avoiding a component-first mindset when creating classes. ↩︎