The Ultimate Long-shadow Sass Mixin

You know the long shadow design trend, right? Although, I’m not even sure this is a trend anymore, everything moves so fast… Anyway, long shadows used to be and / or still are a trendy design trick to give some emphasis to some text or an element.

There is no easy way to create a long shadow in CSS. At the end of the day, either you use some image-based format (an actual image, SVG…) or you rely on text-shadow and box-shadow. The latter options are better because they are nothing but CSS that can be handled very nicely by the browser.

The Problem is: that it is a pain to write. To achieve a clean long shadow effect, you have to build a chain upwards of 50 shadows or so, slightly shifting values one step at a time, possibly the color as well… Completely impractical. This is typically the kind of thing you want to automate with Sass or whatever tool floats your boat.

--ADVERTISEMENT--

Indeed, there have been many attempts at making a long shadow mixin. Among the solutions I have seen, I have noticed problems, including:

  • no way to define the direction;
  • no way to define the number of shadows;
  • only works for text-shadow;
  • overly complicated.

Well fasten your seat belt folks because I have found what I dare to call a clean solution. I have built a ~15 lines long property-agnostic long-shadow function that accepts a direction (as well as a few other arguments).

The API

Because we want our tool to work with both box-shadow and text-shadow properties, we will not actually build a mixin like the title of the article says, but actually a function. Then, you’ll be able to use it like this:

.foo {
    box-shadow: long-shadow($args...);
}

.bar {
    text-shadow: long-shadow($args...);
}

Indeed, box shadows and text shadows are almost identical except that box shadows have a blur parameter that we will not use. Note that Internet Explorer 10+ supports blur for text shadows as well… True story.

Our function needs a couple of things to work:

  • a direction, in a similar fashion to linear-gradient so either a keyword or an angle (in deg, rad, grad or turn);
  • a length;
  • a color;
  • whether or note the shadow should fade (the default is false); this is either a boolean (where true means fading to transparent) or a color to fade to;
  • the number of shadows to compute (the default is 100).
/// Function to generate long shadows (because flat is so has-been).
/// Property-agnostic: works for both `box-shadow` and `text-shadow`.
/// `cos` and `sin` might need to be polyfilled if Compass or any
/// equivalent such as SassyMath is not in use.
///
/// @author Hugo Giraudel
///
/// @link https://unindented.org/articles/trigonometry-in-sass/ Pure Sass `cos` and `sin`
///
/// @param {Direction} $direction
///     Shadow's direction (angle or keyword)
/// @param {Length} $length
///     Shadow's length
/// @param {Color} $color
///     Shadow's color
/// @param {Bool | Color} $fade [false]
///     Whether or not shadow should fade:
///     - `false` means no fading, shadow is `$color`
///     - `true`  means fading from `$color` to transparent
///     - a color means fading from `$color` to `$fade`
/// @param {Number} $shadow-count [100]
///     Number of computed shadows
///
/// @return {List} - List of shadows
///
/// @require {function} Compass/helpers/math/cos
///     http://compass-style.org/reference/compass/helpers/math/#cos
/// @require {function} Compass/helpers/math/sin
///     http://compass-style.org/reference/compass/helpers/math/#sin
///
/// @example scss - Usage
/// .foo {
///   text-shadow: long-shadow(42deg, 1em, #16a085);
/// }
/// .bar {
///   box-shadow: long-shadow(to top left, 150px, hotpink, tomato);
/// }
@function long-shadow($direction, $length, $color, $fade: false, $shadow-count: 100) {}

Building the function

Now let’s get started with the function. The first thing we have to do is convert $direction to an angle if it is a keyword (e.g. to right being 90deg). Doing so is extremely straightforward; we only need a conversion map.

$conversion-map: (
  to top: 180deg,
  to top right: 135deg,
  to right top: 135deg,
  to right: 90deg,
  to bottom right: 45deg,
  to right bottom: 45deg,
  to bottom: 0deg,
  to bottom left: 315deg,
  to left bottom: 315deg,
  to left: 270deg,
  to left top: 225deg,
  to top left: 225deg
);

@if map-has-key($conversion-map, $direction) {
  $direction: map-get($conversion-map, $direction);
}

At this point, there is only one thing left to do (yes, already!): iterating from 1 through $shadow-count, each time computing a new shadow.

// ...

$shadows: ();

@for $i from 1 through $shadow-count {
  // ...

  $shadow: ...;
  $shadows: append($shadows, $shadow, 'comma');
}

@return $shadows;

Now we will need to compute $shadow. At first I thought it would be a pain, but it turns out to be basic trigonometry. We only need to compute the sin of the angle for the x offset and the cos of the angle for the y offset. Then, we multiply both by the length of the long shadow divided by the number of total shadows, multiplied by the current index of $i.

$x: sin(0deg + $direction) * ($i * $length / $shadow-count);
$y: cos(0deg + $direction) * ($i * $length / $shadow-count);

You may wonder why we append the $direction (an angle) to 0deg. Actually, this is a clever way to cast the second value into the first’s unit. In this case, if $direction is expressed in gradients, radians or turns, it is automatically converted in its equivalent in degrees.

Regarding the color, it is a bit trickier. Depending on the value of $fade, we have three options:

  • if $fade is false, then the color is $color;
  • if $fade is true, then we fade from $color to transparent;
  • if $fade is a color, then we fade from $color to $fade.

So unless $fade is false, the color slightly varies at each loop run, so we will need to compute it every time.

// If `$fade` is `false`
$current-color: $color;

// If `$fade` is a color
@if type-of($fade) == 'color' {
  $current-color: mix($fade, $color, ($i / $shadow-count * 100);
// If `$fade` is `true`
} @else if $fade {
  $current-color: rgba($color, 1 - $i / $shadow-count);
}

… or as a one liner:

$current-color: if(not $fade, $color, if(type-of($fade) == 'color',  mix($fade, $color, ($i / $shadow-count * 100)), rgba($color, 1 - $i / $shadow-count)));

So at this point our shadow (to be appended to the shadows list) is:

$shadow: $x $y 0 $current-color;

The whole function

@function long-shadow($direction, $length, $color, $fade: false, $shadow-count: 100) {
  $shadows: ();
  $conversion-map: (
    to top: 180deg,
    to top right: 135deg,
    to right top: 135deg,
    to right: 90deg,
    to bottom right: 45deg,
    to right bottom: 45deg,
    to bottom: 0deg,
    to bottom left: 315deg,
    to left bottom: 315deg,
    to left: 270deg,
    to left top: 225deg,
    to top left: 225deg
  );

  @if map-has-key($conversion-map, $direction) {
    $direction: map-get($conversion-map, $direction);
  }

  @for $i from 1 through $shadow-count {
    $current-step: ($i * $length / $shadow-count);
    $current-color: if(not $fade, $color, if(type-of($fade) == 'color',  mix($fade, $color, ($i / $shadow-count * 100)), rgba($color, 1 - $i / $shadow-count)));

    $shadows: append($shadows, (sin(0deg + $direction) * $current-step) (cos(0deg + $direction) * $current-step) 0 $current-color, 'comma');
  }

  @return $shadows;
}

Examples

.foo {
  text-shadow: long-shadow(
    // Shadow should have an angle of 42 degrees
    $direction: 42deg,
    // Shadow should be contain within a 100x100 box
    $length: 100px,
    // Shadow should start this color
    $color: #16a085,
    // To finish this color
    $fade: #1abc9c
  );
}

long shadow example 1

.bar {
  box-shadow: long-shadow(
    // Shadow should go to bottom right (45deg)
    $direction: to left,
    // With a length of 15em
    $length: 15em,
    // From this color
    $color: #2980b9,
    // To this color
    $fade: #e67e22
  );
}

long shadow example 2

.baz {
  box-shadow: long-shadow(
    // Shadow should have an angle of 25deg
    $direction: -125deg,
    // Spread on 120px
    $length: 120px,
    // From this color
    $color: #8e44ad,
    // To transparent
    $fade: true,
    // With only 10 shadows
    $shadow-count: 10
  )
}

long shadow example 3

Final thoughts

That’s it my friends! I hope you like both the power and the simplicity of this approach. Feel free to suggest any improvement and be sure to have a look at the code on CodePen!

See the Pen The Ultimate Long-shadow Sass Mixin by SitePoint (@SitePoint) on CodePen.

Replies

  1. That's a really neat solution, I like the API, as long as we are sensible with comments it should make sense to other devs who might work on the project. My only gripe is it generates a lot of CSS, but I see no way around that.

    I'd be interested to know how well the compiled code compresses with GZIP, if it's not so good then I guess we should be sensible with our usage.

    I'm currently reading through your blog Hugo and there's some real gems in there, keep them coming smile

  2. It's a great solutions. But c'mon. Long shadows?!

  3. You don’t need Compass for this. :wink: