Using SASS to Create a Lean Utility Library

An alternative to Tailwind CSS

Tailwind CSS is great but requires precious build minutes when using a service like Netlify. This is especially true when using Purge CSS. A faster alternative is to decide which utility classes are useful for your project, and build those with the power of SASS mixins and functions.

Useful Utility Classes

Here are some examples of the types of utlity classes you may want.

Color Classes

Like these:

.bg-light {
  background-color: #fafafa; }

.bg-dark {
  background-color: #030303; }

.bg-primary {
  background-color: #9100b6; }

.c-light {
  color: #fafafa; }

.c-dark {
  color: #030303; }

.c-primary {
  color: #9100b6; }

First create an array of arrays of properties you want to add colors to.

$colorProperties: (
    ('bg-', 'background-color')
    ('c-', 'color')
);

Then decide what your color options are (these can be a simple 1-1 map):

$colorOptions: (
  light: #fafafa,
  dark: #030303,
  primary: #9100b6
);

Here’s what our mixin looks like:

@mixin generateUtilityClasses($properties, $options) {
  @each $item in $properties {
    $classPropertyText: nth($item, 1);
    $cssProperty: nth($item, 2);
    @each $classValueText, $cssValue in $options {
      .#{$classPropertyText}#{$classValueText} {
        #{$cssProperty}: $cssValue;
      }
    }
  }
}

Spacing Utility Classes

Sometimes we want to generate some utility classes like this:

.space-y-xs > * + * {
  margin-top: 0.5rem; }

.space-y-sm > * + * {
  margin-top: 1rem; }

.space-y-md > * + * {
  margin-top: 2rem; }

.space-x-xs > * + * {
  margin-left: 0.5rem; }

.space-x-sm > * + * {
  margin-left: 1rem; }

.space-x-md > * + * {
  margin-left: 2rem; }

Note that after every utility class, there is a > * + * part of the selector. This is what we’ll add to a new, 3rd part of our array:

$gapProperties: (
    ('space-y-', 'margin-top', '> * + *'),
    ('space-x-', 'margin-left', '> * + *')
);

And our properties look like this:

$gapOptions: (
  'xs' : 0.5rem,
  'sm' : 1rem,
  'md' : 2rem
);

Now we’ll include a $selectorAdditional variable as a part of the new mixin:

@mixin generateUtilityClasses($properties, $options) {
  @each $item in $properties {
    $classPropertyText: nth($item, 1);
    $cssProperty: nth($item, 2);
    $selectorAdditional: nth($item, 3);
    @each $classValueText, $cssValue in $options {
      .#{$classPropertyText}#{$classValueText} #{$selectorAdditional} {
        #{$cssProperty}: $cssValue;
      }
    }
  }
}

Display Utility Classes

Some utility classes don’t need a CSS class prefix like these:

.none {
  display: none; }

.inline {
  display: inline; }

.inline-block {
  display: inline-block; }

.block {
  display: block; }

.flex {
  display: flex; }

.grid {
  display: grid; }

Here are the related properties / options vars:

$displayProperty: (
  ('', 'display', ''),
);
$displayOptions: (
  none: none,
  inline: inline,
  inline-block: inline-block,
  block: block,
  flex: flex,
  grid: grid
);

Responsive Utility Classes

It’s also nice to have classes like these:

.none {
  display: none; }

.block {
  display: block; }

@media (min-width: 768px) {
  .sm\:none {
    display: none; }
  .sm\:block {
    display: block; } }

Here’s the associated property and options arrays:

$displayProperty: (
  ('', 'display', ''),
);
$displayOptions: (
  none: none,
  block: block,
);

But now we also want to create a breakpoints map:

$breakpoints: (
    sm: 768px
);

And use another mixin:

@mixin respond($media) {
	@each $name, $width in $breakpoints {
		@if $media == $name {
			@media (min-width: $width) { @content; }
		}
	}
}

Here’s our new mixin, now incorporating the responsive mixin:

@mixin generateUtilityClasses($properties, $options, $responsive: false) {
  @each $item in $properties {
    $classPropertyText: nth($item, 1);
    $cssProperty: nth($item, 2);
    $selectorAdditional: nth($item, 3);

    @each $classValueText, $cssValue in $options {
      .#{$classPropertyText}#{$classValueText} #{$selectorAdditional} {
        #{$cssProperty}: $cssValue;
      }
    }
    @if($responsive) {
      @each $breakpointName, $width in $breakpoints {
        @include respond($breakpointName) {
          @each $classValueText, $cssValue in $options {
            .#{$breakpointName}\:#{$classPropertyText}#{$classValueText} #{$selectorAdditional} {
              #{$cssProperty}: $cssValue;
            }
          }
        }
      }
    }
  }
}

Fancy Utility Classes

Some utility classes are too complex for the above mixin to generate. But we can write custom mixins for custom needs.

For example:

Sometimes you might want to visually shift an element

over here

or

over here!

The classes that are pulling that text are built based on the max-width of the .container element, so this is a great opportunity to make all these classes with a single mixin:

@mixin generateContainerOverrides($containerMaxWidth) {
  .container {
    padding: 0 15px;
    margin: 0 auto;
    max-width: $containerMaxWidth;
  }
  .containerPull {
    &-left {
      margin-left: -15px;
      @media screen and (min-width: $containerMaxWidth) {
        margin-left: calc($containerMaxWidth/2 - 50vw);
      }
    }
    &-right {
      margin-right: -15px;
      @media screen and (min-width: $containerMaxWidth) {
        margin-right: calc($containerMaxWidth/2 - 50vw);
      }
    }
    &-both {
      margin: 0 -15px;
      @media screen and (min-width: $containerMaxWidth) {
        margin: 0 calc($containerMaxWidth/2 - 50vw);
      }
    }
  }
}

You would then generate these classes by running:

@include generateContainerOverrides(70rem);

Abstracting Classes

Once you have your utility clsses, you can optionally abstract them out using SASS extends like this:

.btn {
  @extend .inline-block, .bg-primary, .c-dark;
}