Unique Pairs in SCSS

This article, part of the writing collection, was published on .

In creating a from-scratch grid for a redevelopment project, I was using SCSS to loop through various bits of data to generate a bunch of repetitive CSS, but I realised I had no DRY way of generating a list of unique pairs of items from a given dataset, so I set out to find out how.

[SCSS] 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 SCSS’s purpose, its strengths, and its weaknesses. This could be chalked up to a poor introduction to SCSS; 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, SCSS thrives under very controlled and predictable conditions. Casting a bunch of CSS property values to SCSS variables is all well and good, but only if they’re simple and cohesive with the rest of your stylesheets. Converting your CSS to SCSS 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 SCSS 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 SCSS 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 more time-consuming than generating it automatically from a handful of SCSS variables.

An example Permalink

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 SCSS 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 visually-hidden;
		}
	}
	.hide--above-#{$breakpoint-name} {
		@media (min-width: #{$breakpoint-value}) {
			@include visually-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! Permalink

Here’s the behemoth @function that accepts a SCSS 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.
  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 Permalink

$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-small-to-medium {
	display: none;
}

.from-small-to-large {
	display: none;
}

.from-medium-to-large {
	display: none;
}

From a Map Permalink

$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;
				}
			}
		}
	}
}
.from-small-to-medium {
	@media (min-width: 500px) and (max-width: 750px) {
		display: none;
	}
}

.from-small-to-large {
	@media (min-width: 500px) and (max-width: 1000px) {
		display: none;
	}
}

.from-small-to-gigantic {
	@media (min-width: 500px) and (max-width: 1250px) {
		display: none;
	}
}

.from-medium-to-large {
	@media (min-width: 750px) and (max-width: 1000px) {
		display: none;
	}
}

.from-medium-to-gigantic {
	@media (min-width: 750px) and (max-width: 1250px) {
		display: none;
	}
}

.from-large-to-gigantic {
	@media (min-width: 1000px) and (max-width: 1250px) {
		display: none;
	}
}

Conclusion and Demo Permalink

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:

Sorry, this code snippet failed to load, but you can still check it out over on CodePen!

This is a pretty extreme example of how smart utilisation of SCSS’s 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, sizes, 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.

You can also send an anonymous reply (using Quill and Comment Parade).

2 Responses

  1. 1 Like
  2. 1 Link