I tend to find from project to project I need the same handful of helper classes. Whenever possible I use mixins to generate these helpers due to the ease of maintenance—it's nice to be able to add or remove additional classes to a range.

The Helper Loop

This is the classic mixin I use for generating helper classes, which I just call the helper loop:

// e.g. .a-1 {z-index:1} .a-2 {z-index:2} .a-3 {z-index:3} @mixin helper-loop($start, $stop, $step, $class-name, $property, $unit, $unit-start: false, $unit-step: false, $extend: "") { $i: $start; @if not $unit-start { $unit-start: $start; } @if not $unit-step { $unit-step: $step; } $value: $unit-start; @while $i <= $stop { .#{$class-name}-#{$i} { #{$property}: #{$value}#{$unit}!important; }; @if ($extend != "") { .#{$class-name}-#{$i} { @extend #{$extend} } } $value: $value + $unit-step; $i: $i + $step; } }
undefined
Let's take a look at an example for generating some margin classes. First is the more simple example, where .margin-15 gives a value of margin: 15px: @include helper-loop(0, 15, 5, 'margin', 'margin', 'px');
undefined
Or maybe we want to abstract the numerical value on the helper class from the unit itself, so .margin-4 to get margin: 1rem: @include helper-loop(0, 4, 1, 'margin', 'margin', 'rem', 0, .25);
undefined
With fonts maybe we want to skip the odd numbers: @include helper-loop(8, 36, 2, 'font-size', 'font-size', 'rem', 0.625, 0.125);
undefined

I like to have the number on the end of the helper class line up with the pixel size of a font, as it's easier to visualize what the result will be. So in this case font-size-12 might be 1em, but visually I want that to be the same as 12px.

Sometimes I'll setup two loops next to each other. Maybe I want a few odd number font sizes in that bread in butter range of 11–17 pixels:

@include helper-loop(8, 36, 2, 'font-size', 'font-size', 'rem', 0.625, 0.125); @include helper-loop(11, 17, 2, 'font-size', 'font-size', 'rem', 0.6875, 0.125);
undefined

I like to have some classes to nudge elements using position: relative;, but I really just want to use a single helper class to do this like .top-1. This is where that extend parameter comes in:

.relative { position: relative; } @include helper-loop(1, 5, 1, 'top', 'top', 'px', false, false, ".relative");
undefined

Then all I need is one class to nudge an element up or . I love this one for icons which rarely have the same visual baseline as text:

Then all I need is one class to nudge an element <span class="bottom-2">up</span> or <span class="bottom-2"></span>
undefined

Responsive Classes

Building out responsive classes is more complex. I use a loop in a loop to do this. I also don't use a global mixin for this like the other helpers, preferring to copy, paste and modify an existing loop.

I'll use one of my spacing loops to demonstrate. Let's start with just one helper class to understand what we are creating:

.col-margin-1 > * { margin-left: .25rem; margin-right: .25rem; } .col-margin-1 > *:first-child { margin-left: 0; } .col-margin-1 > *:last-child { margin-right: 0; }
undefined

Having child selectors and psuedo selectors complicates this enough that I can't use my helper loop mixin, and responsive classes further complicate matters. First thing I need is a map of my breakpoints. I like to match my breakpoints to what Bootstrap uses as they're easy to commit to memory:

$bootstrap-breakpoints: ( 'xs': 0, 'sm': 576px, 'md': 768px, 'lg': 992px, 'xl': 1200px, 'xxl': 1400px, );
undefined

I want to be able to pass parameters into a function of some sort and have that spit out all the helper classes for me. That function call will look like this:

@include spacing-loop(.25, 4, 'margin');
undefined

I'm going to be calling this loop a couple of times to generate .col-margin-8, .col-margin-12 and .col-margin-16. So there is a hidden parameter of $spacing-breakpoints which is map of all the breakpoints I want to generate classes for.

$spacing-breakpoints: 'xs', 'sm'; // 'md', 'lg', 'xl', 'xxl';
undefined

Next we need to generate each of the media breakpoints:

@mixin spacing-breakpoint-loop($property, $i, $value, $breakpoints) { @each $breakpoint in $breakpoints { @media (min-width: map-get($bootstrap-breakpoints, $breakpoint)) { @include spacing-styles($property, $i, $value, $breakpoint); } } }
undefined

This means our last mixin has the actual classes and instructions in it:

@mixin spacing-styles($property, $i, $value, $breakpoint) { @if $breakpoint == 'xs' { $breakpoint: ''; } @else { $breakpoint: '-#{$breakpoint}'; } .col-#{$property}#{$breakpoint}-#{$i} > * { #{$property}-left: calc(#{$value}rem / 2); #{$property}-right: calc(#{$value}rem / 2); } .col-#{$property}#{$breakpoint}-#{$i} > *:first-child { #{$property}-left: 0 } .col-#{$property}#{$breakpoint}-#{$i} > *:last-child { #{$property}-right: 0 } }
undefined

The only thing I want to call out here is the check for the smallest possible breakpoint of 'xs'. The media query applies to any screen size which 0px and above, which means that class applies to all screen sizes and we don't need to designate those classes as '-xs-'.

To close this out I'm going to include the entire _spacing.scss file. Keep in mind that I have $bootstrap-breakpoints as global variable for all my scss files, but I'll include here for simplicity:

/* Child Spacing * Stuff to generate a helper classes for setting the spacing on all child elements. */ /* EXAMPLE: .col-margin-1 or .row-margin-1 or .row-padding-1 or .col-margin-12 */ // You get 1, 2, 3, 4, 8, 12, 16. Padding isn't enabled (so enable if needed). Jump to end of file. // This should be a global variable available to all scss files. $bootstrap-breakpoints: ( 'xs': 0, 'sm': 576px, 'md': 768px, 'lg': 992px, 'xl': 1200px, 'xxl': 1400px, ); // Add the breakpoints as they are needed: 'xs', 'sm', 'md', 'lg', 'xl', 'xxl' $spacing-breakpoints: 'xs'; //, 'sm', 'md', 'lg', 'xl', 'xxl'; @mixin spacing-styles($property, $i, $value, $breakpoint) { @if $breakpoint == 'xs' { $breakpoint: ''; } @else { $breakpoint: '-#{$breakpoint}'; } .col-#{$property}#{$breakpoint}-#{$i} > * { #{$property}-left: calc(#{$value}rem / 2); #{$property}-right: calc(#{$value}rem / 2); } .col-#{$property}#{$breakpoint}-#{$i} > *:first-child { #{$property}-left: 0 } .col-#{$property}#{$breakpoint}-#{$i} > *:last-child { #{$property}-right: 0 } .row-#{$property}#{$breakpoint}-#{$i} > * { #{$property}-top: calc(#{$value}rem / 2); #{$property}-bottom: calc(#{$value}rem / 2); } .row-#{$property}#{$breakpoint}-#{$i} > *:first-child { #{$property}-top: 0 } .row-#{$property}#{$breakpoint}-#{$i} > *:last-child { #{$property}-bottom: 0 } } @mixin spacing-breakpoint-loop($property, $i, $value, $breakpoints) { @each $breakpoint in $breakpoints { @media (min-width: map-get($bootstrap-breakpoints, $breakpoint)) { @include spacing-styles($property, $i, $value, $breakpoint); } } } @mixin spacing-loop($amount, $steps, $property, $start: false) { $i: 1; @if $start { $i: $start; } $value: $amount; @while $i <= $steps { @include spacing-breakpoint-loop($property, $i, $value, $spacing-breakpoints); $value: $value + $amount; $i: $i + 1; } } // Example: .col-margin-1 @include spacing-loop(.25, 4, 'margin'); @include spacing-loop(2, 8, 'margin', 8); // For 2rem @include spacing-loop(3, 12, 'margin', 12); // For 3rem @include spacing-loop(4, 16, 'margin', 16); // For 4rem // Enable if needed. // @include spacing-loop(.25, 4, 'padding');
undefined