Sass Maps vs. Nested Lists

James Steinbach
Share

The title of this post may be surprising to some of you. If you are a Sass veteran, you may remember the days (pre-Ruby-Sass-3.3) of using lists of lists to emulate nested arrays of data. (Ruby) Sass 3.3 added a new data type called maps. Lists of lists could hold complex data in a nested format, but without key-value pairing. Maps added key-value pairs and let us create arrays of data.

With the advent of maps, many of us Sass users started putting everything into maps (and for good reason!). All your breakpoint widths, color values, grid layouts, type scales and other responsive typography details can go into maps!

Now that Sass has maps with key-value pairs, is there a good reason to use a list of lists? One theoretical reason would be backwards-compatibility: if your Sass might be maintained by a developer with an older version installed, lists will help them. In practice, however, Sass versions are often controlled by a package.json or other project config, and the Ruby gem can be updated with just one command (gem update sass).

A more practical reason that you might choose to use nested lists instead of a map is typing less. Let’s compare a map and a nested list to see how they compare in their own syntax, and in how we’d loop through them.

Comparing Syntax

For our example, let’s create a data structure that controls responsive typography. It will store four breakpoints (well, one is the default smallest view). For each breakpoint, we’ll store min-width, max-width, a base font-size, and a base line-height.

Complex Map Syntax

Here’s how we’d store that data in a map. One large map will contain four keys (breakpoint labels) whose values are maps of the variables we need to store and use. In a readable format like this, we’ve got a little more than 450 characters and 26 lines.

$breakpoint-map: (
  small: (
    min-width: null,
    max-width: 479px,
    base-font: 16px,
    vertical-rhythm: 1.3
  ),
  medium: (
    min-width: 480px,
    max-width: 959px,
    base-font: 18px,
    vertical-rhythm: 1.414
  ),
  large: (
    min-width: 960px,
    max-width: 1099px,
    base-font: 18px,
    vertical-rhythm: 1.5
  ),
  xlarge: (
    min-width: 1100px,
    max-width: null,
    base-font: 21px,
    vertical-rhythm: 1.618
  )
);

Nested List Syntax

The nested list for storing this same data is much shorter. We no longer have keys attached to our data however, so we have to rely on looping through it, or calling it with nth() functions. That said, it’s much shorter than the map: less than 180 characters and only 6 lines.

$breakpoint-list: (
  (small, null, 479px, 16px, 1.3),
  (medium, 480px, 959px, 18px, 1.414),
  (large, 960px, 1099px, 18px, 1.5),
  (xlarge, 1100px, null, 21px, 1.618)
);

Comparing Loops

When it comes to typing out our data structure, typing lists takes about a third of the time of typing a map. If we need to loop through those values, however, how does that compare?

Complex Map Loop

We can use the following code to loop through the top level items in this map:

@each $label, $map in $breakpoint-map {}

The two variables at the beginning of this line ($label and $map) are dynamically assigned as the loop iterates through the data in the map. Each top-level piece of data has two components: a key and a value. We’re assigning the key to $label and the value (which is a nested map) to $map. Inside this loop, we can use the variables $label and $map and they’ll automatically represent the key and value of the current entry.

That loop will iterate four times, once for each nested map. To get useful data out of the nested map, however, we’ll need to use the map-get() function. This function takes two parameters – the name of the map and the name of the desired key – and returns the value associated with that key. It’s the Sass equivalent to PHP’s $array['key'] and $object->key or JavaScript’s object.key syntax.

To iterate through all the sub-maps with @each and assign their values to useful variables with map-get(), we end up with a 6-line, 220-character loop.

@each $label, $map in $breakpoint-map {
  $min-width: map-get($map, min-width);
  $max-width: map-get($map, max-width);
  $base-font: map-get($map, base-font);
  $vertical-rhythm: map-get($map, vertical-rhythm);
}

Nested List Loop

Nested lists really make looping efficient. With maps, we had to assign a map to a dynamic loop variable and then use map-get() to assign all its values to variables, but with lists, we can quickly assign all values to variables.

Since each item in the top-level list has the same five values in the same order, we can instantly assign each of those to a dynamic variable to use inside our loop. With those variables, we don’t need to use map-get() to assign sub-values to usable variables. The loop we need for nested lists is only two lines and less than 100 characters.

@each $label, $min-width, $max-width, $base-font, $vertical-rhythm in $breakpoint-list {
}

Nested List Warnings

Nested lists are a major developer performance win: overall, you’ll probably type less than half as much as you’d type if you were using a map. However, there’s a reason maps were added to Sass: they provide a feature lists don’t: key-value mapping.

Missing Values

If you’re going to rely on nested lists, you must be absolutely certain you know how many items each list will hold and what order they’ll be in. Let’s look at what would happen in our example above if we left out an item in one of our lists:

$breakpoint-list: (
  (small, null, 479px, 16px, 1.3),
  (medium, 480px, 959px, 18px, 1.414),
  (large, 960px, 1099px, 18px, 1.5),
  (xlarge, 1100px, 21px, 1.618)
);

p {
  @each $label, $min-width, $max-width, $base-font, $vertical-rhythm in $breakpoint-list {
    @if $min-width {
      @include breakpoint( $min-width ) {
        font-size: $base-font;
        line-height: $vertical-rhythm;
      }
    } @else {
      font-size: $base-font;
      line-height: $vertical-rhythm;
    }
  }
}

If we try running that code, the last list will break. It will correctly assign ‘xlarge’ to $label and ‘1100px’ to $min-width, but then it will assign ’21px’ to $max-width and ‘1.618’ to $base-font, leaving $vertical-rhythm blank. As a result, we end up with an invalid font-size declaration and a missing line-height property in the last breakpoint. Additionally, Sass doesn’t report an error for this, so we’d have no idea if things worked out or not. If we had tried using the max-width for a media query, we would have ended up with the font-size value (just 21px) – that would be a pretty useless max-width, I think!

If we’d used maps instead, the map-get() function would’ve given us what we needed even if one of the values were missing. That’s our trade-off: what we gain in simplicity and speed with lists, we lose in specificity and error-proofing with maps.

Querying a Specific List

A related concern with using nested lists is querying for a specific list. Since maps have keys, you can quickly access any of the sub-maps with map-get():

$medium-map: map-get($maps, medium);

To get the data from the medium list in the nested lists, we need a more complex function:

@function get-list($label) {
  @each $list in $breakpoint-list {
    @if nth($list, 1) == $label {
      @return $list;
    }
  }
  @return null;
}
$medium-list: get-list(medium);

That function loops through all the lists in $breakpoint-list, checks the first value for the label we want, and returns the list if it finds a match. If it gets to the end of the @each loop without finding a match, it’ll return null. It’s basically a quick homemade interpretation of map-get() for lists that use the first value as a faux key.

Missing Map Functions

Sass has quite a few useful functions for dealing with maps: those same functions don’t exist for nested lists. For example, you can use map-merge() to add additional key-value pairs to a map. Using map-merge() with shared keys will update the value for shared keys. You can add a new list using join() or append(), but faking the update feature of map-merge() would require another custom Sass function.

Another useful map function is map-has-key(). This function is useful for validating any custom function that relies on map-get(). There’s no comparable function for lists, however.

You can use SassyLists to mimic map functions with lists. (This library provided these functions before Sass added map support.)

Conclusion

Maps are more powerful than lists because of they use key-value pairs. The additional Sass map functions provide useful ways to find data and validate map values.

Nested Sass lists can be quicker to write and maintain, but they’re probably not as well-suited for error-checking or detailed querying as maps. Most of the time, I believe maps are the better option, in spite of the increased verbosity. For smaller chunks of code and single-use loops, I’ll occasionally use a nested list, but maps work better for project-wide settings and data storage.

Have you compared maps and nested lists in any of your work, or refactored code to prefer one over the other? Share your experience in the comments!

You can see the code used for this tutorial in this Sassmeister gist.