SCSS Random Values

It’s nice for app & site designs to have a little variability. Uneveness used with care can communicate a handmade feeling, for example slight waviness instead of perfectly regular lines in a diagram makes it look hand drawn.

Box diagram I made in Exalidraw. Notice the unevenness in the line thickness throughout.

When it comes to implementing uneveness, developers use random numbers to add or subtract variable amounts to numerical design features.

Did you know Sass / SCSS has a function (math.random) for producing random numbers that you can use in your stylesheets? It's great but it can be confusing to use.

Imagine you want to show a text highlighter effect on some important points in a block of text. You might implement it with some Sass like this:

p > span.highlight {
  position: relative;
  z-index: 0;
  &::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: yellow;
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
    transform: rotate(3deg);
  }
}

One issue with this approach is that it’s too regular to look hand-written. It's a little more obvious when you see several highlights adjacent to each other:

Lorem ipsumdolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

If I introduce randomness to the angle of the highlight each place it's applied, the result looks more convincing:

Lorem ipsumdolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Implementing that improvement wasn’t as simple as applying Sass' math.random() to the CSS transform angle. Here was my first attempt:

+ @function random-angle($from, $to) {
+   $value: math.random($to - $from + 1);
+   $value: $from + ($value - 1);
+   // math.random() ignores units, so add it back by type coercion
+   @return $value + 0deg;
+ }

  p > span.highlight {
    position: relative;
    z-index: 0;
    &::before {
      content: "";
      position: absolute;
      z-index: -1;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: yellow;
      clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
-     transform: rotate(3deg);
+     transform: rotate(random-angle(-3, 3));
    }
  }

But defining one CSS class with a random value isn’t enough to achieve the effect we want. Everywhere .highlight is used, it will apply the same random angle to the highlight box. Think of it as a constant variable assignment. The evaluated random value is bound to the CSS class once, not each place that class is used.

To get a random value for each highlight location, we need a separate class for each location. First, I'll move the highlight effect into an SCSS mixin:

+ @mixin highlight-effect {
+   position: relative;
+   z-index: 1;
+   &::before {
+     content: "";
+     position: absolute;
+     z-index: -1;
+     top: 0;
+     left: 0;
+     width: 100%;
+     height: 100%;
+     background-color: yellow;
+     clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
+     transform: rotate(random-angle(-3, 3));
+   }
+ }

  p > span.highlight {
-   position: relative;
-   z-index: 0;
-   &::before {
-     content: "";
-     position: absolute;
-     z-index: -1;
-     top: 0;
-     left: 0;
-     width: 100%;
-     height: 100%;
-     background-color: yellow;
-     clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
-     transform: rotate(random-angle(-3, 3));
-   }
+   @include highlight-effect;
  }

Next, I'll create a separate class for each highlight location:

- p > span.highlight {
+ p > span {
+   &.highlight-1, &.highlight-2, &.highlight-3, &.highlight-4 {
      @include highlight-effect;
+   }
  }

And then explicitly override the transform rotation in each class, giving each class it's own random angle:

  p > span {
    &.highlight-1, &.highlight-2, &.highlight-3, &.highlight-4 {
      @include highlight-effect;
    }
+   &.highlight-1 {
+     &::before {
+       transform: rotate(random-angle(-3, 3));
+     }
+   }
+   &.highlight-2 {
+     &::before {
+       transform: rotate(random-angle(-3, 3));
+     }
+   }
+   &.highlight-3 {
+     &::before {
+       transform: rotate(random-angle(-3, 3));
+     }
+   }
+   &.highlight-4 {
+     &::before {
+       transform: rotate(random-angle(-3, 3));
+     }
+   }
  }

Of course that pattern can get out of hand really quickly. What happens when we have 8 locations in a paragraph we'd like to highlight, instead of 4? A nice improvement would be to use a loop to generate the classes:

+ .highlight-base {
+   @include highlight-effect;
+ }
+
+ @mixin generate-highlight-variants($count) {
+   @for $i from 1 through $count {
+     &.highlight-#{$i} {
+       @extend .highlight-base;
+       &::before {
+         transform: rotate(random-angle(-3, 3));
+       }
+     }
+   }
+ }

  p > span {
-   &.highlight-1, &.highlight-2, &.highlight-3, &.highlight-4 {
-     @include highlight-effect;
-   }
-   &.highlight-1 {
-     &::before {
-       transform: rotate(random-angle(-3, 3));
-     }
-   }
-   &.highlight-2 {
-     &::before {
-       transform: rotate(random-angle(-3, 3));
-     }
-   }
-   &.highlight-3 {
-     &::before {
-       transform: rotate(random-angle(-3, 3));
-     }
-   }
-   &.highlight-4 {
-     &::before {
-       transform: rotate(random-angle(-3, 3));
-     }
-   }
+   @include generate-highlight-variants(8);
  }

Notice that instead of using @include highlight-effect; within the loop, I added a helper class .highlight-base that each variant class in the loop @extend's. That makes a smaller output stylesheet because @extend will de-duplicate shared CSS rules in the output, meaning:

.highlight-1,
.highlight-2,
.highlight-3,
.highlight-4 {
  /* common highlight styles */
}

instead of:

.highlight-1 {
  /* common highlight styles */
}
.highlight-2 {
  /* common highlight styles */
}
.highlight-3 {
  /* common highlight styles */
}
.highlight-4 {
  /* common highlight styles */
}

With that, here's the final SCSS:

@use "sass:math";

// ...

@function random-angle($from, $to) {
  $value: math.random($to - $from + 1);
  $value: $from + ($value - 1);
  // math.random() ignores units, so add it back by type coercion
  @return $value + 0deg;
}

@mixin highlight-effect {
  position: relative;
  z-index: 1;
  &::before {
    content: "";
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: yellow;
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
    transform: rotate(random-angle(-3, 3));
  }
}

.highlight-base {
  @include highlight-effect;
}

@mixin generate-highlight-variants($count) {
  @for $i from 1 through $count {
    &.highlight-#{$i} {
      @extend .highlight-base;
      &::before {
        transform: rotate(random-angle(-3, 3));
      }
    }
  }
}

p > span {
  @include generate-highlight-variants(8);
}

And example HTML usage:

<p>
  <span class="highlight-1">Lorem ipsum</span> dolor
  <span class="highlight-2">sit amet</span>,
  <span class="highlight-3">consectetur adipiscing</span> elit,
  <span class="highlight-4">sed do eiusmod</span> tempor
  <span class="highlight-1">incididunt ut labore</span> et dolore magna aliqua.
  Ut enim ad minim veniam,
  <span class="highlight-2">quis nostrud exercitation</span> ullamco
  <span class="highlight-3">laboris nisi ut aliquip</span> ex ea commodo
  consequat.
</p>

gives you:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.