DEV Community

Cover image for Three Tricks to Make an Atomic Sass Framework
Max Antonucci
Max Antonucci

Posted on • Edited on

Three Tricks to Make an Atomic Sass Framework

As a front-end developer, my go to way of writing manageable CSS for a while has been using the BEM naming architecture. It helps create specific, scoped class names that are easy to identify and avoid lots of style overlap. For quite a while it seemed like the only sustainable way for me to write CSS.

Then I read John Polacek's "Rethinking CSS" slideshow and learned about Atomic CSS. I highly encourage reading the entire thing (regardless of your reaction), but the short version is it's writing CSS almost entirely with helper classes based on visual function. I was so intrigued about it that I decided to code my own Atomic CSS framework with Sass, and want to share how I overcame some of the biggest challenges I encountered.

But first, let me write this post isn't about debating whether Atomic CSS is a good idea or not. I have no interest in being asked to defend another's idea from angry developers who aren't being told they must use it (again...). I still don't yet even know which I personally prefer. This post just looks at some Sass tricks I think other developers could find useful. I'm not responding to yells about why Atomic CSS sucks; please do that elsewhere.

With those disclaimers done, let's get to the actual article!

1) Creating a Global Class Namespace

The first (and simplest) step is always making a namespace. It's not essential to any framework, but it's worth it to avoid conflicts with anything else.

I've only recently realized that it's better to define a global namespace in one place. Especially with Atomic CSS, where it'll be used on virtually every class. With Sass, that means using a variable!

$g-nmsp: ".zz";

// The `$g-` prefix is a reminder that it's a global Sass variable.
Enter fullscreen mode Exit fullscreen mode

With the namespace defined, a little interpolation magic puts it to use:

#{$g-nmsp}-relative { position: relative; }
#{$g-nmsp}-absolute { position: absolute; }
#{$g-nmsp}-static { position: static; }
#{$g-nmsp}-fixed { position: fixed; }
Enter fullscreen mode Exit fullscreen mode

And the output is like so:

.zz-relative { position: relative; }
.zz-absolute { position: absolute; }
.zz-static { position: static; }
.zz-fixed { position: fixed; }
Enter fullscreen mode Exit fullscreen mode

Hooray! Going forward it's now easy to change the namespaces on my helper class army.

2) Creating Base Styles alongside Utility Classes

While I can accept Atomic CSS uses lots of classes, one worry I have is when it's too much. An example is typography - I could make classes for font-size, margin, font-family for each header. But then I'd have to apply those classes to each header every time!

My compromise is applying each property to the base element and a helper class. This way I can make standard header styles while making helper classes for the same values.

The best way to organize all this? Sass maps!

2.1) Define the typography values

The first step is identifying the base typography elements - header tags, paragraph tags, and any others. Then I put them in nested maps based on the style properties. Below is an example for font sizes:

$typography-map: (
  font-size: (
    h1: decimal-round(modular-scale(5), 2),
    h2: decimal-round(modular-scale(4), 2),
    h3: decimal-round(modular-scale(3), 2),
    h4: decimal-round(modular-scale(2), 2),
    h5: decimal-round(modular-scale(1), 2),
    h6: decimal-round(modular-scale(0), 2),
    large: decimal-round(modular-scale(0), 2),
    p: decimal-round(modular-scale(0), 2),
    small: decimal-round(modular-scale(-1), 2),
    tiny: decimal-round(modular-scale(-2), 2)
  ),

  // ... Line-heights, margins, and more below
}
Enter fullscreen mode Exit fullscreen mode
2.2) Generate both styles from these values

This is the basic pattern for generating both base and helper style values, again with fonts-sizes as the example.

@each $label, $font-size in map-get($typography-map, font-size) {
  #{$label},
  #{$g-nmsp}-font-size-#{$label} { font-size: $font-size; }
}
Enter fullscreen mode Exit fullscreen mode

The output is exactly what I want:

h1,
.zz-font-size-h1 { font-size: 3.05rem }

h2,
.zz-font-size-h2 { font-size: 2.44rem }

h3,
.zz-font-size-h3 { font-size: 1.95rem }

// And it continues...
Enter fullscreen mode Exit fullscreen mode

Doing this with all typography styles, the base styles layer together and I have nice-looking default headers. I also have helper classes for those same properties to override header styles when needed or use anywhere else.

You may wonder why I use Sass maps. It's because they're an efficient way to organize property/value pairs together by category. I'll know where all my values are and they won't leak into anything else. Plus it's easy to loop through them and generate CSS in a DRY way.

3) Generating Base and Responsive Spacing Classes

One of the toughest challenges was making responsive classes. The classic example is margin values that can be applied with or without breakpoints, like so:

.zz-mt-base { margin-top:1rem }

@media (min-width: 30rem) {
  .zz-mt-base-sm { margin-top : 1rem }
}

@media (min-width: 50rem) {
  .zz-mt-base-md { margin-top : 1rem }
}

@media (min-width: 75rem) {
  .zz-mt-base-lg { margin-top : 1rem }
}
Enter fullscreen mode Exit fullscreen mode

I'm adding a base value of 1rem to an element's top margin, or mt for short. I can also do so only after certain breakpoints. This would be the same for other margin values like double or half, for other margin locations like bottom, and all my breakpoints.

Sounds simple, but the challenge is doing this in a DRY way.

3.1) Define the breakpoints

My approach to more complex issues is always starting small. First is just defining the breakpoints.

Once again, I turn to Sass maps! Here I also have a mixin for getting values from the map.

$breakpoint-map: (
  xs: 0px,
  sm: rem(480px),
  md: rem(800px),
  max: rem(1200px)
);

@mixin larger-than($point-name) {
  @if ($point-name != 'xs') {
    $width: map-get($breakpoint-map, $point-name);

    @media (min-width: $width) { @content; }
  } @else {
    @content;
  }
}

Enter fullscreen mode Exit fullscreen mode

This way, making responsive styles is easy and semantic:

.some-selector {
    // Base styles go here!

    @include larger-than(md) {
        // Styles for screens larger than 800px go here!
    }
}
Enter fullscreen mode Exit fullscreen mode

You may wonder about that breakpoint for 0px. That's not even a breakpoint, and the mizing ignores it so it seems useless. Hold onto that thought, as I'll explain why in a few steps.

3.2) Define the spacing values

Next step was creating the margin units with...yep, a Sass map.

My usual approach is defining a base spacing value, then doing simple math and Sass functions to create larger and smaller values off it. This makes spacing values easy to change, more cohesive, but still allows for overrides (like the none value).

$g-base-spacing: rem(16px);

$spacing-map: (
  none   : 0,
  quad   : decimal-round(($g-base-spacing * 4), 2),
  triple : decimal-round(($g-base-spacing * 3), 2),
  double : decimal-round(($g-base-spacing * 2), 2),
  oneHalf : decimal-round(($g-base-spacing * 1.5), 2),

  base   : decimal-round($g-base-spacing, 2),

  half   : decimal-round(($g-base-spacing / 2), 2),
  third  : decimal-round(($g-base-spacing / 3), 2),
  quart  : decimal-round(($g-base-spacing / 4), 2)
);
Enter fullscreen mode Exit fullscreen mode

Hooray, all my spacing values are set! No mixin or function for pulling them needed, which will also be explained in the next step.

3.3) Making the responsive utility classes.

Now for the fun part. My ideal solution for making these classes does several things:

  1. Uses the map data for both breakpoints and spacing values
  2. Only need to write the difference class names once
  3. Creates utility classes with and without breakpoints
  4. Only add the breakpoint labels to responsive classes

Also there may be styles that need the breakpoint data, but not the spacing data. For example, if I wanted the margin to just be auto on certain breakpoints.

The final answer is bulky, so I'll go step-by-step.

First I just looped through both maps. It's important to start with the breakpoint loop - the margin: auto classes can be defined there, and classes that need spacing are in both loops.

@each $bp-label, $bp in $breakpoint-map {

 @include larger-than($bp-label) {
    @each $label, $length in $spacing-map {
      // Margin classes with spacing values go here
    }
  }

  // Auto margin classes go here
}

Enter fullscreen mode Exit fullscreen mode

The second step was creating labels only for spacing classes with breakpoints. My first version defined the same spacing classes twice - once with breakpoint labels and once without. But I found a DRYer solution which needed that xs breakpoint. The trick adding the breakpoint label to each class, but make it an empty string for those without breakpoints.

@each $bp-label, $bp in $breakpoint-map {

 $bp-label-final: '';

 @if ($bp-label != 'xs') { $bp-label-final: '-' + $bp-label; }

 @include larger-than($bp-label) {
    @each $label, $length in $spacing-map {
      // Margin classes that use the different spacing values go here
    }
  }

  // Auto margin classes that don't need the different spacing lengths go here
}
Enter fullscreen mode Exit fullscreen mode

Now all I needed was a single loop using this $bp-label-final variable in the class names. It adds nothing to base classes and adds the needed label to responsive ones. I can also use this trick for margin: auto classes. I can define all the different margin utility classes once and get everything I need!

@each $bp-label, $bp in $breakpoint-map {

  $bp-label-final: '';

  @if ($bp-label != 'xs') { $bp-label-final: '-' + $bp-label; }

  @include larger-than($bp-label) {
    @each $label, $length in $spacing-map {
      // Margin
      #{$g-nmsp}-m-#{$label}#{$bp-label-final} { margin: $length; }
      #{$g-nmsp}-mt-#{$label}#{$bp-label-final} { margin-top: $length; }
      #{$g-nmsp}-mr-#{$label}#{$bp-label-final} { margin-right: $length; }
      #{$g-nmsp}-mb-#{$label}#{$bp-label-final} { margin-bottom: $length; }
      #{$g-nmsp}-ml-#{$label}#{$bp-label-final} { margin-left: $length; }

      #{$g-nmsp}-mx-#{$label}#{$bp-label-final} {
        margin-right: $length;
        margin-left: $length;
      }

      #{$g-nmsp}-my-#{$label}#{$bp-label-final} {
        margin-top: $length;
        margin-bottom: $length;
      }
    }
  }

  // Auto margin classes that don't need the different spacing lengths

  #{$g-nmsp}-mx-auto#{$bp-label-final} {
    margin-right: auto;
    margin-left: auto;
  }

  #{$g-nmsp}-my-auto#{$bp-label-final} {
    margin-top: auto;
    margin-bottom: auto;
  }

  #{$g-nmsp}-mt-auto#{$bp-label-final} { margin-top: auto; }
  #{$g-nmsp}-mr-auto#{$bp-label-final} { margin-right: auto; }
  #{$g-nmsp}-mb-auto#{$bp-label-final} { margin-bottom: auto; }
  #{$g-nmsp}-ml-auto#{$bp-label-final} { margin-left: auto; }
}

Enter fullscreen mode Exit fullscreen mode

Solution found! I now have a DRY way to get the output from the start of the section. I can easily change the breakpoints, spacing values, or class syntax without any unneeded repetition. Even better, this trick can also be used for other styles, like padding utility classes!

Conclusion

These three tricks together took care of most of the challenges with my Atomic Sass framework. The rest was mostly writing stand-alone classes and deciding what to include. So they're useful to keep in mind for anyone else planning to write their own too.

I'll end on a note about my own feelings about Atomic CSS. After reading about it I had a similar visceral rejection many other devs do. For me it was because BEM had helped me so much and didn't want to "abandon it." Plus adding lots of classes to a page can appear unwieldy and confusing. It was a tough pill to swallow.

But looking at the benefits in CSS file size, the "single responsibility principle," and the ease of updating and customizing page styles, my mind has opened up more. At the very least I think it's an effective architecture for certain projects, like those with lots of devs with rougher CSS skills.

That realization is what led me to make an Atomic Sass like this in the first place. If I ever do fully come around to Atomic CSS, I'll want to be ready.

Top comments (0)