Building a Linear-gradient Mixin in Sass

Kitty Giraudel

First things first: you should probably not use a preprocessor, here Sass, to add vendor prefixes to CSS properties. This is not the kind of thing you want to bring to your code base. It should typically be the kind of thing you want to treat with something like Autoprefixer.

Not only will Autoprefixer do a far better job than you at printing the accurate prefixes (I am looking at you, developers who write -ms-linear-gradient and -moz-border-radius), but it’s also a breeze to update, maintain and scale. Everything is done from a configuration file, not from within the stylesheets.

Anyway, I’m done with the disclaimer. In this article, I would like to show you how to write a mixin that prints a linear-gradient as well as a fallback color and the old version for WebKit (still using the old angle notation). While you might not necessarily need it, I believe you could learn quite a few things from this code.

Note: please let me take this article as an opportunity to remind you that linear gradients only need the WebKit prefix, and none of the others. Please stop printing obsolete and absent prefixed gradients in your stylesheets. :)

Building the mixin

Building a mixin to create a gradient from arguments, convert an angle, prefix the linear-gradient property and everything else may sound like a heavy task but you’ll see that it is actually very easy to achieve as long as you endeavor to keep things simple.

It has occurred to me that people writing gradient prefixes often try to parse and control everything. This is certainly not a good idea. Not only is it quite tedious to do, but it also ends up with crowded complex code that is absolutely not necessary.

Let’s recap what we need to do:

  • Output the first color as a fallback in case CSS gradients are not supported.
  • Output the -webkit- prefixed value while converting the angle to old syntax.
  • Output the standard declaration as given.

Then our mixin needs two things: the direction, and any number of extra arguments that will be printed the way they are being passed.

/// Mixin printing a linear-gradient
/// as well as a plain color fallback
/// and the `-webkit-` prefixed declaration
/// @access public
/// @param {Keyword | Angle} $direction - Linear gradient direction
/// @param {Arglist} $color-stops - List of color-stops composing the gradient
@mixin linear-gradient($direction, $color-stops...) {
  background: nth(nth($color-stops, 1), 1);
  background: -webkit-linear-gradient(legacy-direction($direction), $color-stops);
  background: linear-gradient($direction, $color-stops);

Don’t worry too much about the legacy-direction function and the fallback for now. We will come back to those later. Now, the direction is optional in linear-gradient notation. For instance, the following declaration is perfectly valid:

.selector {
  background-image: linear-gradient(hotpink, tomato);

This will use 180deg as a default value for direction, producing a vertical gradient (horizontal for old WebKit, due to the change in the syntax). In order to keep our mixin fully compliant with the usual notation from CSS, we need to allow this. Let’s see what’s going on if we rewrite our previous example using our mixin:

.selector {
  @include linear-gradient(hotpink, tomato);

In this case, $direction is hotpink, and $color-stops is an arglist of a single value: tomato. Because of this, we need our mixin to define a default value for $direction if it’s not a valid direction (either an angle or a directional keyword). In this case, the current $direction, a color, needs to be prepended to the $color-stops list.

It may sound a bit complicated but trust me everything is going to be clear with the following code snippet:

@mixin linear-gradient($direction, $color-stops...) {
  // Direction has been omitted and happens to be a color-stop
  @if is-direction($direction) == false {
    $color-stops: $direction, $color-stops;
    $direction: 180deg;

  background: nth(nth($color-stops, 1), 1);
  background: -webkit-linear-gradient(legacy-direction($direction), $color-stops);
  background: linear-gradient($direction, $color-stops);

Let’s recap: if the $direction argument happens not to be a direction (we’ll inspect the is-direction function later on), we prepend it to the $color-stops list (because it’s a color-stop), and we redefine $direction to 180deg, the default value. Easy, right?

We are almost done, let me just explain how the fallback works because it might look weird. We want the first color value of the $color-stops list to be the fallback color in case linear gradients are not supported.

One might think nth($color-stops, 1) would be enough, but what if the first value happens to be a 2-items long list containing a color and a color stop (e.g. hotpink 42%)? Because of this, we make sure always to grab the color value, using nth(nth($color-stops, 1), 1), also known as the first value of the first value of $color-stops.

Testing if a value is a direction

The first line of our mixin tests whether the $direction argument is a valid direction as per CSS specifications. There is no built-in function for this, so we need to make one. It is not very difficult per se, but it turns out there are quite a few ways to define a direction in CSS. If we stick to linear gradients, here are the different options:

  • a keyword value among to top, to top right (or to right top), to right, to bottom right (or to right bottom), to bottom, to bottom left (or to left bottom), to left, to left top (or to top left);
  • an angle either in deg, rad, grad or turn.

So checking whether a value is a direction is as simple as testing if it either a valid keyword, or a number with a valid unit.

/// Test if `$value` is a valid direction
/// @param {*} $value - Value to test
/// @return {Bool}
@function is-direction($value) {
  $is-keyword: index((to top, to top right, to right top, to right, to bottom right, to right bottom, to bottom, to bottom left, to left bottom, to left, to left top, to top left), $value);
  $is-angle: type-of($value) == 'number' and index('deg' 'grad' 'turn' 'rad', unit($value));

  @return $is-keyword or $is-angle;

Converting angle to legacy syntax

The WebKit version relies on the old syntax, where:

So we need our legacy-direction function to:

  • return the accurate keyword if the input is a keyword;
  • return the accurate angle if the input is an angle;
  • throw an error if the input is neither a keyword nor an angle.
/// Convert a direction to legacy syntax
/// @param {Keyword | Angle} $value - Value to convert
/// @require {function} is-direction
/// @require {function} convert-angle
/// @throw Cannot convert `#{$value}` to legacy syntax because it doesn't seem to be a direction.;
@function legacy-direction($value) {
  @if is-direction($value) == false {
    @error "Cannot convert `#{$value}` to legacy syntax because it doesn't seem to be a direction.";

  $conversion-map: (
    to top          : bottom,
    to top right    : bottom left,
    to right top    : left bottom,
    to right        : left,
    to bottom right : top left,
    to right bottom : left top,
    to bottom       : top,
    to bottom left  : top right,
    to left bottom  : right top,
    to left         : right,
    to left top     : right bottom,
    to top left     : bottom right

  @if map-has-key($conversion-map, $value) {
    @return map-get($conversion-map, $value);

  @return 90deg - $value;

Note: as of writing, LibSass 3 does not support @error yet. If you want to increase support for LibSass, feel free to replace @error with @warn. I currently maintain a compatibility table between the different versions of Sass (Ruby, and LibSass)

Using it

Alright! We’re done with the code. All that’s left is using our mixin to make sure it works.

.selector-1 {
  @include linear-gradient(#31B7D7, #EDAC7D);

.selector-2 {
  @include linear-gradient(to right, #E47D7D 0%, #C195D3 50%, #4FB4E8 100%);

.selector-3 {
  @include linear-gradient(42deg, #B58234 0%, #D2B545 50%, #D7C04D 50.01%, #FFFFFF 100%);
.selector-1 {
  background: #31B7D7;
  background: -webkit-linear-gradient(-90deg, #31B7D7, #EDAC7D);
  background: linear-gradient(180deg, #31B7D7, #EDAC7D);

.selector-2 {
  background: #E47D7D;
  background: -webkit-linear-gradient(left, #E47D7D 0%, #C195D3 50%, #4FB4E8 100%);
  background: linear-gradient(to right, #E47D7D 0%, #C195D3 50%, #4FB4E8 100%);

.selector-3 {
  background: #B58234;
  background: -webkit-linear-gradient(48deg, #B58234 0%, #D2B545 50%, #D7C04D 50.01%, #FFFFFF 100%);
  background: linear-gradient(42deg, #B58234 0%, #D2B545 50%, #D7C04D 50.01%, #FFFFFF 100%);

Final thoughts

That’s it folks. As you can see, it was not that hard to build and did not involve some weird parsing and detection. You can play with the code on SassMeister.