Making a ski coat with Sass maps

When Sass 3.3 was released earlier this year, one of the big additions was the introduction of the maps data type. And while I understood the purpose of maps, I never fully appreciated how much time they could save you… until I took a ski trip with some friends a few months back.

We were waiting in line for the ski lift when I saw a guy wearing a brightly colored jacket:

ski jacket

“Dope jacket, brah!”

So I did what any rational person would do under the circumstances: I snapped a picture like a creeper and decided to recreate the pattern in CSS.

The Final Product

Here’s how it turned out:

See the Pen Ski coat pattern in CSS using Sass maps by lukeelmers (@lukeelmers) on CodePen.

The Approach

Before I started, I established a few basic rules for myself:

  1. The pattern needs to be created entirely in CSS.
  2. There should be fine-grained control over each triangle’s color.
  3. I wanted to add a fun hover effect, so each triangle needed to be individually animatable.

The Markup

Originally I planned to create a 10 x 10 stack of zero-width <div>s, each with a different border-color on every side. This would require 100 <div>s to house all 400 triangles.

However, I quickly realized this approach wouldn’t allow me to animate each triangle individually, since you can’t use transform on a single border side. This meant each of the 400 triangles would need to live in its own <div>.

I feel I should take a moment to point out that this isn’t clean markup. In fact, this is something you’d probably want to avoid on a production site. But for the purposes of this experiment, it serves as a convenient example of how Sass maps can save a ton of time.

Since I’m lazy and didn’t want to write out each of the 400 <div>s, I opted to use Slim to create them for me. Slim is a templating language that makes generating repetitive sets of html a breeze, much like Haml or Jade.

Here’s how I structured the html using Slim:

div class="wrap"
  - for num in (1..100)
    div class="daddy"
        div class="baby north n-#{ num }"
        div class="baby east e-#{ num }"
        div class="baby south s-#{ num }"
        div class="baby west w-#{ num }"

This basically loops through and generates a set of 100 <div>s with the class daddy, all of which live within a wrapper <div>. Each daddy contains 4 child <div>s with the class baby. Each baby is assigned a class name based on a cardinal direction (north, east, south, west). And every baby is also given a unique class consisting of the initial for the cardinal direction (n, e, s, w), followed by the number of its daddy.

The output looks like this:

<div class="wrap">
    <div class="daddy">
        <div class="baby north n-1"></div>
        <div class="baby east e-1"></div>
        <div class="baby south s-1"></div>
        <div class="baby west w-1"></div>
    </div>
    <!-- ...98 more of these... -->
    <div class="daddy">
        <div class="baby north n-100"></div>
        <div class="baby east e-100"></div>
        <div class="baby south s-100"></div>
        <div class="baby west w-100"></div>
    </div>
</div>

The idea here is that each triangle in the pattern will live in its own baby <div>. The cardinal directions help identify which border side we are talking about (border-top is north, border-bottom is south, etc). And the number identifies which of the 100 daddy squares we should be looking at.

So the selector .e-1 would target the right border (aka east triangle) of the 1st daddy square in the upper left of the page. We could then use border-right-color to set the color of this triangle.

The CSS

I’m using some basic CSS to handle layout of the boxes, including a $size Sass variable to set the size of the individual boxes:

$size: 5em;
html, body { height: 100%; background: black; }
.wrap {
  width: $size*10;
  height: $size*10;
}
.daddy {
  position: relative;
  float: left;
  width: $size;
  height: $size;
}
.baby {
  position: absolute;
  border: $size/2 solid transparent;
}

The Problem

If our little experiment were a real project, this is typically the point where panic would set in for me. Do I really have to write out 400 selectors for every triangle, and then manually pick a color for each?

The short answer: nope.

The longer answer…

Sass Maps Save Lives

Sass maps are basically comma-separated sets of key/value pairs:

$map: (key1: value1, key2: value2, key3: value3);

You can use maps to store data that can be iterated over using a Sass loop. Each key in a map can only have one value, but according to the Sass documentation, that value may be a list.

So in the case of our triangle selectors, we could house our data as a nested list within a map:

$red: #fd594d;
$red-tiles: ( 
  n : (1, 3, 5),  
  e : (2, 4, 6),
  s : (8),
  w : (10)
);

What we’re doing here is creating a Sass variable $red-tiles that stores a map. The map pairs each cardinal direction (n, e, s, w) with a nested Sass list of numbers. These numbers correspond with the daddy squares we were talking about earlier. We’ve also created a $red variable with the hex code for our color.

Now we can create a mixin that sets a border-color for each triangle, given our $red color and $red-tiles map:

@mixin tile-colors($color, $map) {             /* 1 */
  @each $key, $value in $map {                 /* 2 */
    $direction: null;                          /* 3 */
      @if $key == 'n' { $direction: top; }     /* 4 */
      @if $key == 'e' { $direction: right; }
      @if $key == 's' { $direction: bottom; }
      @if $key == 'w' { $direction: left; }
    @for $i from 1 to length($value)+1 {       /* 5 */
      $number: nth($value, $i);                /* 6 */
        .#{$key}-#{$number} {                  /* 7 */
          border-#{$direction}-color: $color;  /* 8 */
      }
    }
  }
}

Here’s how everything is working:

  1. Create mixin called tile-colors() that accepts a color & map as arguments.
  2. For each key/value pair in the map, do the following:
  3. Create a new variable called $direction with a value of null.
  4. Assign a top/right/bottom/left value to the $direction variable, based on the map key.
  5. Loop through the list of values for that key.
  6. Assign the value to a $number variable. (Use +1 to ensure the last item doesn’t get dropped).
  7. Generate a selector using the map key & number.
  8. Set a border color, using the $direction variable to determine the correct side.

So if we call @include tile-colors($red, $red-tiles);, it will output the following:

.n-1 {
  border-top-color: #fd594d;
}
.n-3 {
  border-top-color: #fd594d;
}
.n-5 {
  border-top-color: #fd594d;
}
.e-2 {
  border-right-color: #fd594d;
}
.e-4 {
  border-right-color: #fd594d;
}
.e-6 {
  border-right-color: #fd594d;
}
.s-8 {
  border-bottom-color: #fd594d;
}
.w-10 {
  border-left-color: #fd594d;
}

Generating Colors

The only problem we haven’t solved yet is how to avoid manually inputting a hex value for each individual color in the pattern.

Since the pattern really consists of variations on a few basic colors (red, blue, green, yellow, orange), this is a perfect excuse to use the Sass darken() function. Given a hex value and a percentage, it will return that color with the lightness decreased by the percentage amount.

If we combine darken() with the random function in Sass, we can update our mixin to output a randomly darkened version of our our base color.

@mixin tile-colors($color, $map, $variation) {
  @each $key, $value in $map {
    $direction: null;
      @if $key == 'n' { $direction: top; }
      @if $key == 'e' { $direction: right; }
      @if $key == 's' { $direction: bottom; }
      @if $key == 'w' { $direction: left; }
    @for $i from 1 to length($value)+1 {
      $number: nth($value, $i);
        .#{$key}-#{$number} {
          $rand: random($variation);
          border-#{$direction}-color: darken($color, $rand);
      }
    }
  }
}

By adding the $variation argument which gets passed through to the random() function, we can control how dark our base color is allowed to get. The higher the number, the more variation we’ll see from the original color.

So, given the HTML and CSS we’ve already created, if we call @include tile-colors($red, $red-tiles, 50); we will begin to see some variation in color.

Animations

I decided to add an aperture-like hover animation using CSS transforms, and included a few new variables so it could be adjusted easily. I won’t dive into the details in this post, but here’s where I ended up:

$aperture: 4;
$rotation: 20deg;
$scale: 1.5;
.daddy:hover {
  .baby  { transition: transform 0.2s ease-out; }
  .north { 
    transform: translateX($size/$aperture) 
      translateY(0)  
      rotate($rotation) 
      scale($scale); 
  }
  .east  { 
    transform: translateX(0) 
      translateY($size/$aperture)  
      rotate($rotation) 
      scale($scale); 
  }
  .south { 
    transform: translateX(-$size/$aperture) 
      translateY(0)  
      rotate($rotation) 
      scale($scale); 
  }
  .west { 
    transform: translateX(0) 
      translateY(-$size/$aperture)  
      rotate($rotation) 
      scale($scale); 
  }
}

Since I used a separate <div> for each border side, it is super easy to animate the triangles individually.

Wrapping Up

All that’s left is to create a series of maps for each of our base colors. Then we just do the tedious work of combing through the triangles, determining what base color to assign to each in order to emulate our ski coat, and adding those values to the Sass map for that color.

While this was certainly a time-consuming process, it didn’t take nearly as long as writing out each selector one at a time and hand-picking a hex value for every single triangle!

I’m sure there are even more efficient ways to accomplish this — so I’d encourage you to check out the pen, play around with it, and let me know what you think.

Helpful Reading

Sass Documentation: Maps
Sass Maps Are Awesome by Jason Garber
Using Sass Maps by Hugo Giraudel
Removing Sass Duplication by Kyle Fiedler
Using Nested Sass Maps for TypeSetting by Elijah Manor