Unique Pairs in Sass

Sass is the most mature, stable, and powerful professional grade CSS extension language in the world.

Sass (sass-lang.com)

CSS developers usually fall into one of two camps when it comes to preprocessors: a loving embrace or a cold shoulder. While I have an obvious bias, I get the sense that a portion of the cold shoulder folks perhaps hold a fundamental misunderstanding of Sass’ purpose, its strengths, and its weaknesses. This could be chaulked up to a poor introduction to Sass; it is not a tool that can be mindlessly slapped on top of CSS with the expectation that things will improve through its sheer use.

Like writing careful CSS, Sass thrives under very controlled and predictable conditions. Casting a bunch of CSS property values to Sass variables is all well and good, but only if they’re simple and cohesive with the rest of your stylesheets. Simply converting your CSS to Sass provides you with no wins unless it is done with consideration; in fact, without consideration, I would argue that doing so would add complexity—complexity which I feel can stain the first impression developers get of preprocessors. What purpose does Sass serve if it can never surpass the feature set and limitations of CSS?

The purpose lies, for me, in:

  • simplification of CSS concepts
  • lower mental overhead
  • automation of tedious/repetitive CSS

If your code does the opposite of any of those things, you should re-evaluate what you’re trying to achieve to work out the path of least resistance.

Be wary of Sass which appears more complex or lengthier than its compiled CSS counterpart. It’s only worth the luxury of not having to manage something manually if the pre-compiled code is easy, if not easier, to understand than the compiled output. An example of where this would work might be within a grid system, when defining and maintaining the styles for multiple columns and layouts of columns in CSS is clearly more time-consuming than generating it automatically from a handful of Sass variables.

An example

Let’s say I want to be able to show and hide content based on screen size, and I want some level of minute control around these screen sizes and how they’re used. I’ve got to create a series of CSS classes which hide content based on some given media query breakpoints. Let’s define a small Map of breakpoint names (to reference in the class names) and their respective pixel values (to reference in the media queries).

$breakpoints: (
    "small":     500px,
    "medium":    750px,
    "large":    1000px,
    "gigantic": 1250px
);

The Sass needs to be able to handle any number of breakpoints, not just an arbitrary number, so we’re going to have to use @each and/or @for to create some loops. We need to be able to say below A, above B, above A and below B, etc. do something.

So using a BEM naming methodology, we’ll use a base class of .hide and extend it like so:

  • .hide--below-small
  • .hide--above-medium
  • .hide--small-medium
  • .hide--medium-large
  • .hide--large-gigantic

The single breakpoint below A and above B variations are quite straightforward to generate, and do not require extensive logic or filtering of the dataset to generate the CSS:

@each $breakpoint-name, $breakpoint-value in $breakpoints {
    .hide--below-#{$breakpoint-name} {
        @media (max-width: #{$breakpoint-value}) {
            @include hidden;
        }
    }
    .hide--above-#{$breakpoint-name} {
        @media (min-width: #{$breakpoint-value}) {
            @include hidden;
        }
    }
}

But the between A and B variations are a bit more involved. We’ll need something to loop through $breakpoints and create a Map of pairs, which we can loop through in turn to create our rules based on the pairings.

We can actually determine how many unique pairs there will be with a simple mathematical formula, letting n represent the length of the dataset:

n(n−1)⁄2

So in our case, in which we have 4 breakpoints:

4(4−1)⁄2 = 6

We can expect 6 unique pairs from a list of 4 items.

Here’s the function!

Here’s the behemoth @function that accepts a Sass List or Map and spits out all the unique pairs to do with whatever necessary (and I guarantee promise think it’s less complex than building it manually):

@function unique-pairs($data) {
    @if not $data or not (type-of($data) == list or type-of($data) == map) {
        @warn "unique-pairs() expects either a single List or single Map dataset.";
        @return false;
    }

    $unique-pairs: ();
    $seen: ();

    @if type-of($data) == list {
        @each $first in $data {
            $seen: append($seen, $first);
            @each $second in $data {
                @if $first != $second and not index($seen, $second) {
                    $unique-pair: ($first, $second);
                    $unique-pairs: append($unique-pairs, $unique-pair);
                }
            }
        }
    }

    @else if type-of($data) == map {
        @each $first-key, $first-value in $data {
            $seen: append($seen, $first-key);
            @each $second-key, $second-value in $data {
                @if $first-key != $second-key and not index($seen, $second-key) {
                    $unique-pair: (
                        ($first-key: $first-value), ($second-key: $second-value)
                    );
                    $unique-pairs: append($unique-pairs, $unique-pair);
                }
            }
        }
    }

    @else {
        @warn "unique-pairs() expects either a List or Map.";
        @return false;
    }

    @return $unique-pairs;
}

Let’s go through this piece-by-piece so we can undertand what’s going on.

@if not $data or not (type-of($data) == list or type-of($data) == map) {
    @warn "unique-pairs() expects either a single List or single Map dataset.";
    @return false;
}

We begin by doing some error-checking by confirming two things:

  1. if the input has been passed to the @function
  2. if the input is either of the List or Map type

Next, we instantiate two Maps:

$unique-pairs: ();
$seen: ();
  1. $unique-pairs will be returned by this function and will contain the unique pairs, obviously.
  2. $seen is used to keep track of how far into the original dataset has been iterated to prevent duplication.

Next, we’ll perform some operations in the case where the dataset is a List:

@if type-of($data) == list {
    @each $first in $data {
        $seen: append($seen, $first);
        @each $second in $data {
            @if $first != $second and not index($seen, $second) {
                $unique-pair: ($first, $second);
                $unique-pairs: append($unique-pairs, $unique-pair);
            }
        }
    }
}

Here, we’re looping through the List, and for each item, looping through the List once more. By keeping track of the items we’ve iterated over in the outer loop, we’re able to build a Map of pairs from the List items which contains no duplicates or pairs of the same item.

Next, we’ll do the same, but for a Map of data:

@else if type-of($data) == map {
    @each $first-key, $first-value in $data {
        $seen: append($seen, $first-key);
        @each $second-key, $second-value in $data {
            @if $first-key != $second-key and not index($seen, $second-key) {
                $unique-pair: (
                    ($first-key: $first-value), ($second-key: $second-value)
                );
                $unique-pairs: append($unique-pairs, $unique-pair);
            }
        }
    }
}

The code in this block is nearly identical to the one for Lists, with some small changes to how the Map’s keys and values are paired and passed into $unique-pairs.

Now let’s look at how the @function is actually used.

From a List

$list:
    "small",
    "medium",
    "large";

@each $unique-pair in unique-pairs($list) {
    $unique-pair-first:  nth($unique-pair, 1);
    $unique-pair-second: nth($unique-pair, 2);
    .from-#{$unique-pair-first}-to-#{$unique-pair-second} {
        display: none;
    }
}

From a Map

$map: (
    "small":     500px,
    "medium":    750px,
    "large":    1000px,
    "gigantic": 1250px
);

@each $unique-pair in unique-pairs($map) {
    $map-first:  nth($unique-pair, 1);
    $map-second: nth($unique-pair, 2);
    @each $map-first-key, $map-first-value in $map-first {
        @each $map-second-key, $map-second-value in $map-second {
            .from-#{$map-first-key}-to-#{$map-second-key} {
                @media (min-width: #{$map-first-value}) and (max-width: #{$map-second-value}) {
                    display: none;
                }
            }
        }
    }
}

Conclusion and Demo

While the @function that powers all this jazz is lengthy and intimidating, I think it’s pretty clear that we’re saving ourselves some typing and mental overhead. Because the @function is doing the brunt of the heavy-lifting here, we’ve afforded ourselves the luxury of only having to touch the List/Map of breakpoints (and respective pixel values) in order to build out CSS for the entire grid, including complex class and media query combinations.

Here’s a small demo of the @function on CodePen, which I recommend opening in a new tab/window so you can resize properly:

    
    Check out this CodePen!

This is a pretty extreme example of how smart utilisation of Sass’ features makes writing and maintaining CSS easier (and more satisfying), but you can start on this path in a much smaller capacity. Investigate what kind of wins you’ll achieve through abstracting your colours, measures, sets of styles, etc. and proceed, carefully, to organise and DRY out your code. If you’re unsure where to start with refactoring, Harry Roberts has written some excellent articles about refactoring.

🗓 published on
📚 reading length ~1150 words
🏷 tagged under
🔗 shorturl repc.co/a4ot1


Remapping Ranges in Sass Note from 22 September 2017