How To Write Programmatic Functions With Sass

Leah
General thoughts

Writing CSS with Sass has an incredible amount of versatility in regards to mixins, helpers, variables, and functions. In the years of experimentation, we’ve found some diverse solutions to programmatically generating design patterns with minimal markup, limiting variation in available styles.

The benefit of limited variation in a framework is a consistent set of design guidelines, allowing multiple developers to work on a project without inconsistencies in the UI. We design code to be reusable and easily modifiable for global changes in the future, but most cases demand a stylesheet that most of the team shouldn’t have to interact with or change to generate new pages.

This tutorial will establish how we design reusable functions and mixins that generate utility classes, then cover the more complex logic we’ve automated into our workflow.

Establish your UI’s font-sizes

Let’s start with font sizes. We’ll use a spread that looks consistent but has enough variation to establish hierarchy between definitions. The base font-sizes for our project will be structured so small text will be 14px, base will be 16px, then 18px, 24px, etc. Since 16px is the root font-size for most browsers we’ll use store 100% as our base scale.

$root-font-size: 100% !default;

From there, we pick scales that appropriately represent the other options we want to provide and store them in a variable array. We use percentages for accessibility which allows accurate scaling when zooming in on the page or changing the default font size:

// %'s are calculated with
//   `new-font-size / base-font-size * 100`
$sizes: 87.5%, 100%, 112.5%, 150%, 187.5%, 250%, 275%, 362.5%, 450% !default;

Something to note is that our variables all end with !default. This allows us to override these in other stylesheets without modifying our base file which we reference often.

Building the function

Now that the variable is defined, we need a function to utilize and output each step as a usable class. Since we’re storing these in an array variable, we can use the nth() feature provided by Sass (Note that Sass starts arrays at 1, not 0). If we wanted to get the first item in that list, we could use nth($sizes, 1). To keep the code named properly and reusable, we could do the following:

@function size($i) {
  @return nth($sizes, $i);
}

To use this, we would simply reference the function as a value on font-size, passing through the index of our size:

.font-size-1 {
  font-size: size(1);
}

If you’re following along this would return font-size: 87.5%;. All that remains is creating classes that use the function to get the appropriate scale.

Generating utility classes with mixins

The point of creating a UI-Kit is reusability. A developer shouldn’t have to write CSS or understand your functions to utilize the logic we’ve just defined, so let’s output some useful classes for each font size:

@mixin font-sizes {
  // Generate the classes by iterating through the array
  //  $i becomes the index, which we will interpolate for
  //  the class name and the index for each function call
  @for $i from 1 through length($sizes) {
    .font-size-#{$i} {
      font-size: size($i);
    }
  }
}

// Output the mixin's classes
@include font-sizes;

This would output the following:

.font-size-1 {
  font-size: 87.5%;
}
.font-size-2 {
  font-size: 100%;
}
.font-size-3 {
  font-size: 112.5%;
}
...

One other thing of note here is that we used @for instead of @each to iterate through the array. The reason for this is that we can’t utilize the index for the class names or pass the index to our function with @each without complicating and dirtying our variable array. Using @each would require key/value pairs that would force us to perform far more queries and parsing of data. Just look at the array structure in comparison:

$sizes: (1: 87.5%, 2: 100%, 3: 112.5%, 4: 150%, 5: 187.5%, 6: 250%, 7: 275%, 8: 362.5%, 9: 450%) !default;

The caveat of using @for is the additional need of $i through length($sizes) which gets the length of the array but the additional cost in calculation is minimal.

Why bother with all the markup?

Writing the markup for generating classes may seem complex, but requires minimal work to make large shifts in a framework’s base styling. The time spent initially framing out options will offset the startup time of new projects or project refreshes and can be customized to your team’s preferences with ease.

If you're looking for a copy/paste solution or want to play around with the final version of this code until this point, <a href="https://codepen.io/leaharmstrong/pen/MWWvvJz" target="_blank">check it out on CodePen</a>




Advanced customization for an optimized workflow

If you want to go the extra mile with these functions and mixins to utilize this logic with some added versatility, here are some tips we use when building frameworks.

Leave your mixins without defined class names

Sometimes class names are a preference of a team or project. To make this code more reusable, change the mixin to use suffixes and define separators or implement BEM formatting so it can be used in a variety of ways:

// Goes between the base class name and the generated suffix
$class-name-separator: "-" !default; // or "--" for BEM

@mixin font-sizes {
  // Generate the classes by iterating through the array
  @for $i from 1 through length($sizes) {
    &#{$class-name-separator}#{$i} {
      font-size: size($i);
    }
  }
}

// Output the mixin's classes inside a wrapper class:
.font-size {
  @include font-sizes;
}

With this approach, our output would create classes with a .font-size-1, .font-size-2 naming convention by utilizing the & (parent selectors) and interpolating a class name separator between the class and index. If your team prefers shorter class names for the wrapper (.fs) and use BEM style separators (--), you’d get .fs--1, .fs--2 for your generated class names. The customization is simple and quick to change on a per-project basis with only a single line of code.

Including Zeroes Semantically

We often need the option to include a 0 in our array if we’re generating padding or margin classes so we do our best to include them in our defaults just in case they’re needed elsewhere. While we could just add 0% to the beginning of our $sizes array, referencing the function with size(1) would return 0% which insn’t clear and may make adverse changes in an existing project. We can’t use size(0) as is due to nth() expecting a positive integer either, and our @for array will stop one short.

Let’s start by creating adding 0% and a variable ($sizes-zeroed-index) to instruct the functions that our array needs support for 0:

$sizes: 0%, 87.5%, 100%, 112.5%, 150%, 187.5%, 250%, 275%, 362.5%, 450% !default;
$sizes-zeroed-index: true !default;
$class-name-separator: "-" !default;

@function size($i) {
  @if $sizes-zeroed-index {
    // Equivalent of setting starting index to 0
    // If 0 is passed and `$sizes-zeroed-index` is enabled, `0` becomes `1` when getting the value by index.
    $i: $i + 1;
  }
  // Return the requested nth from $sizes
  @return nth($sizes, $i);
}

With this change, we can now indicate that size() will include what we’ll call a ‘zeroed-index’, meaning size(0) will return 0% and size(1) will return 87.5%. Our classes will be easier to generate programmatically this way later, and 0 is more indicative of what it will return.

Let’s update the mixin to handle the additional changes:

@mixin font-sizes {
  // Define starting variables
  $starting-index: 1;
  $length: length($sizes);

  // Sets the starting index to 0 and reduces length by 1
  //   to compensate for index offset
  @if $sizes-zeroed-index {
    $starting-index: 0;
    $length: $length - 1;
  }

  // Generate the classes by iterating through the array
  @for $i from $starting-index through $length {
    &#{$class-name-separator}#{$i} {
      font-size: size($i);
    }
  }
}

// Output the mixin's classes
.font-size {
  @include font-sizes;
}

Once those changes are complete, the following should be output when the mixin is called:

.font-size-0 {
  font-size: 0%;
}
.font-size-1 {
  font-size: 87.5%;
}
.font-size-2 {
  font-size: 100%;
}
.font-size-3 {
  font-size: 112.5%;
}
...

To see a summary of where we are so far or experiment with this feature, check out a CodePen example here

Using customization to automate more utility classes

Let’s take a look at how we can take the things we learned for font-sizes and make them useful for things like margin, padding, and line-height.

Let’s set up variable arrays for $sides, $spacing, and $border-widths to add to our $sizes:

$root-font-size: 100% !default;
$class-name-separator: "-" !default;
$sides: (a: all, t: top, r: right, b: bottom, l: left, v: vertical, h: horizontal) !default;
$spacing: 0%, 25%, 62.5%, 100%, 187.5%, 275%, 375%, 562.5%, 750% !default;
$spacing-zeroed-index: true !default;
$sizes: 0%, 87.5%, 100%, 112.5%, 150%, 187.5%, 250%, 275%, 362.5%, 450% !default;
$sizes-zeroed-index: true !default;

Rem Calculator

From there we’re going to need a function to convert our % (or px) values into rem. You might be asking why we’re using percentages and converting them to rem, but for us, it’s mostly consistency with font-sizes, and the ability to scale or make changes to em or ex later if desired.

The logic following is pretty robust, but it handles input from plenty of areas in our markup and converts them to rem in areas where percentage values aren’t what we want (I’ll try to leave plenty of notes):

// Remove the unit of a length
@function strip-unit($number) {
  @if type-of($number) == 'number' and not unitless($number) {
    @return $number / ($number * 0 + 1);
  }
  @return $number;
}

// Convert % or px values to rem
@function rem-calc($int) {
  // In case an auto is passed from an array, just return it.
  @if $int == "auto" {
    @return auto;
  }
  // Ready to store the value
  $calculated-value: null;
  // Get the root unit type
  $root-unit: unit($root-font-size);
  // Get the root value without unit
  $root-unitless: strip-unit($root-font-size);
  // Get the passed value's unit type
  $int-unit: unit($int);
  // Get the passed value without unit
  $int-unitless: strip-unit($int);
  // If the root unit type isn't px or %, output an error
  @if $root-unit != 'px' and $root-unit != '%' {
    @error "$root-font-size must use `%` or `px` in order for `rem-calc()` to function";
  }
  // If the passed value's unit type isn't px or % output an error
  @if $int-unit != 'px' and $int-unit != '%' {
    @error "`rem-calc() requires a `%` or `px` value to function";
  }

  // This will do the math to calculate what each value should be compared to the $base-font-size
  // If the base font size is a %, then multiply it by 16px
  //   100% font size = 16px in most all browsers
  @if $root-unit == '%' {
    @if $int-unit == '%' {
      $calculated-value: $int-unitless / $root-unitless;
    } @else {
      $calculated-value: $int-unitless / (($root-unitless / 100) * 16);
    }
  } @else {
    @if $int-unit == '%' {
      $calculated-value: (($root-unitless * $int-unitless) / $root-unitless) / 100;
    } @else {
      $calculated-value: $root-unitless / $int-unitless;
    }
  }

  // Append `rem` to new value
  $calculated-value: $calculated-value + rem;
  @return $calculated-value;
}

Space Helpers

Now that we have that working, here’s our space generator which will output rem-based measurements consistent with our design patterns. It is essentially the same as size(), but it accesses a different array, and doesn’t return % values (size() can be modified the same way if you desire):

// Get the spacing measurement from the provided argument
// EXAMPLE:
// If you want 0 spacing on the object: `space(0)` #RETURNS 0
// If you want the base/smallest spacing on the object: `space(1)` #RETURNS 4px
@function space($i) {
  @if $spacing-zeroed-index {
    // Equivalent of setting starting index to 0
    // If 0 is passed and `$spacing-zeroed-index` is enabled, `0` becomes `1` when getting the value by index.
    $i: $i + 1;
  }
  // Get the value from the array
  $value: nth($spacing, $i);

  // Pass the value to our rem-calculator and return the value
  @return rem-calc($value);
}

Since this isn’t specific to padding or margins, it can be used anywhere you want those measurements used:

padding: space(1); // .25rem
margin: space(3); // 1rem

To make these usable for other developers, we’ll need to generate helper classes. Earlier we defined $sides which will choose which orientations we create classes for. We’re going to be creating ones for top, bottom, left, right, vertical and horizontal. This next section of markup is a monster of conditionals and cross-checks, but it will allow us to generate padding and margin utility classes with appropriate suffixes for each of the designated sizes with ease:

// Creates side modifier classes for sizes
// `$attr`: The css attribute to be generated | (margin, padding, etc)
// `$size`: The size key for the generator | (0, 1, 2, 3)
// `$value`: The value for the css attribute | (1px, 10px)
@mixin sides($attr, $size, $value) {
  // `$sn-modifier` will be the side's name to be appended to the `$attr`
  $sn-modifier: '';
  // Each `$s`(side key) and `$sn`(side value/name)
  @each $s, $sn in $sides {
    // Vertical sides
    @if $s == v {
      $sn-modifier: (-top, -bottom);

    } @else if $s == h {
      $sn-modifier: (-left, -right);
    // Unless the side in the loop is `a`(all), create a side modifier to use on the attribute
    } @else if $s != a {
      // This basically becomes `-top`, `-right`, `-left`, `-bottom` variables for the attribute output
      $sn-modifier: -#{$sn};
    }
    // Appends a side to the class name
    // `.margin` becomes `.margin-top` for the margin-top class helper
    &-#{$sn} {
      // Appends a size adition to the class name
      // `.margin-top` becomes `.margin-top-1` for a the size 1 class helper
      &#{$class-name-separator}#{$size} {
        @if ($s == v or $s == h) {
          // Iterate trough each side if horizontal or vertical being run
          @each $sn-m in $sn-modifier {
            // Get the attribute, apply the modifier, output the rendered size based on the size keys provided
            #{$attr}#{$sn-m}: $value;
          }
        } @else {
          // Get the attribute, apply the modifier, output the rendered size based on the size key provided
          #{$attr}#{$sn-modifier}: $value;
        }
      }
    }
  }
}

// Creates space classes for padding and margin
@mixin space($type) {
  // Define starting variables
  $starting-index: 1;
  $length: length($spacing);

  // Sets the starting index to 0
  //   and reduces length by 1
  //   to compensate for index offset
  @if $spacing-zeroed-index {
    $starting-index: 0;
    $length: $length - 1;
  }
  @for $i from $starting-index through $length {
     // sides($attr, $size, $value, $side, $append)
     @include sides($type, $i, space($i));
  }
}

With this logic running and implemented into our project, creating side helpers is as simple as defining the class and passing the mixin with an argument for the value being adjusted:

.padding {
  @include sides(padding);
}
.margin {
  @include sides(margin);
}

The above will output a TON of utility classes which can be summarized as any combination of the following:

.{ margin | padding }-{ all | top | right | bottom | left }-{ 0-8 }

With these, your padding and margin options require no coding on the developer’s part while allowing the rapid development of a project with a set of rigid guidelines.

Final Summary

This is a scaled-down version of the framework we use which has more generators and options, but this can all be easily customized to fit any project. We continue to improve and optimize these flows with additions such as media query breakpoints for margin and padding, creating grid systems, color palettes and plenty more. If you want all of the logic we built above in a single document for testing, check out this CodePen to play around.

If your Rails application needs a facelift, reach out to see how we can help you with a custom framework or UI Kit for your project.

Have a project we can help with?
Let's Talk

Get Started Today