Sass in the Real World: book 2 of 4

Function v. Mixin

In Sass, functions and mixins are very similar. They have the following characteristics in common:

  1. They both can access variables or accept arguments
  2. They both allow for logic to be applied to the variables and return a certain value

The areas were they divert from each other are:

  1. Functions will only return a single value (in any object type supported in Sass; numbers, strings, colors, booleans, lists, and maps)
  2. mixins are able to process logic and output CSS rule with attributes and values
  3. The @content directive cannot be used with functions

Because of their similarities and despite their differences, functions and mixins are sometimes used in an incorrect manner. For example, let's take a look at the following mixin that allows to add a border to a block element:

$border-position-all: 'all' !default;
$border-default-size: 1px !default;
$border-default-pattern: solid !default;
$border-default-color: #000 !default;

@mixin add-border($border-position: $border-position-all, 
  $border-size: $border-default-size, $border-pattern: $border-default-pattern, 
  $border-color: $border-default-color) {

  @if $border-position == 'all' {
    border: $border-size $border-pattern $border-color;
  } @else {
    border-#{$border-position}: $border-size $border-pattern $border-color;
  }
}

This mixin can be written as function in the following manner:

$border-default-size: 1px !default;
$border-default-pattern: solid !default;
$border-default-color: #000 !default;

@function add-border-fn($border-size: $border-default-size,
  $border-pattern: $border-default-pattern, $border-color: $border-default-color) {

  $border: $border-size $border-pattern $border-color;

  @return $border;
}

This may go under the rule of "Just because you can doesn't mean that you should". The rule that we follow when it comes to implementing an abstracted logic with a custom function or a mixin is the following:

  1. Functions should be used for a reusable logic that does a repeated calculation and returns a certain value. For example, the emCalculator function that we created earlier in this chapter where it takes a pixel value and converts it to em values.
  2. Mixins should be used for reusable CSS logic, style, or series of properties and values.

Here is an example of a mixin created to handle the variety media query strategies needed for a responsive website:

// ==== media queries ======================================================
//   EXAMPLE Media Query for Responsive Design.
//   This example overrides the primary ('mobile first') styles
//   Modify as content requires.
//   ==========================================================================

//Responsive
//-----------------------------
$small-screen-min-width: em(320px) !default;
$small-screen-max-width: em(767px) !default;
$medium-screen-min-width: em(768px) !default;
$medium-screen-max-width: em(1024px) !default;
$large-screen-min-width: em(1025px) !default;

$screen: "only screen" !default;
$small: "only screen and (min-width:#{$small-screen-min-width}) and (max-width:#{$small-screen-max-width})" !default;
$medium: "only screen and (min-width:#{$medium-screen-min-width}) and (max-width:#{$medium-screen-max-width})" !default;
$large: "only screen and (min-width:#{$large-screen-min-width})" !default;
$landscape: " and (orientation: landscape)" !default;
$portrait: " and (orientation: portrait)" !default;


@mixin respond-to($media, $orientation: false) {
  @if $media == smartphone {
    @if $orientation {
      @if $orientation == landscape {
        @media #{$small} #{$landscape} { @content}
      } @else if $orientation == portrait {
        @media #{$small} #{$portrait} { @content}
      }
    } @else {
      @media #{$small} { @content}
    }
  } @else if $media == tablet {
    @if $orientation {
      @if $orientation == landscape {
        @media #{$medium} #{$landscape} { @content}
      } @else if $orientation == portrait {
        @media #{$medium} #{$portrait} { @content}
      }
    } @else {
      @media #{$medium} { @content}
    }
  } @else if $media == desktop {
    @media #{$large} {@content}
  }
}

// ==== End media queries ======================================================

After reviewing this mixin further we have decided to abstracted some of the elements of this mixins, particularly the area where we are handling the logic to build the @media label based on the media type that is being passed to the mixin and the function. Here is the mixin, re-written:

// ==|== media queries ======================================================
//   EXAMPLE Media Query for Responsive Design.
//   This example overrides the primary ('mobile first') styles
//   Modify as content requires.
//   ==========================================================================

//Responsive
//-----------------------------
$screen: "only screen" !default;
$landscape: " and (orientation: landscape)" !default;
$portrait: " and (orientation: portrait)" !default;

$media-query-sizes: (
  small: (
    min: em(320px),
    max: em(767px)
  ),
  medium: (
    min: em(768px),
    max: em(1024px)
  ),
  large: (
    min: em(1025px)
  )
);

@function media-label($media, $orientation: false) {
  @if(not map-has-key($media-query-sizes, $media)){
    @warn "the $media value needs to be one of the following #{map-keys($media-query-sizes)}";
    @return false;
  }

  $media-sizes: map-get($media-query-sizes, $media);

  $media-label: $screen + " and (min-width:#{map-get($media-sizes, 'min')})";

  @if(length($media-sizes) > 1) {
   $media-label: $media-label +  " and (max-width:#{map-get($media-sizes, 'max')})";
  }

  @if $orientation {
    @if $orientation == landscape {
      $media-label: $media-label + $landscape;
    } @else {
      $media-label: $media-label + $portrait;
    }
  }

  @return $media-label;
}

@mixin respond-to($media, $orientation: false) {
  $media-query-label: media-label($media, $orientation);

  @if $media-query-label {
    @media #{media-label($media, $orientation)} {
      @content
    }
  }
}

// ==== End media queries ======================================================

As you can see, we have abstracted the following variables:

$small-screen-min-width: em(320px) !default;
$small-screen-max-width: em(767px) !default;
$medium-screen-min-width: em(768px) !default;
$medium-screen-max-width: em(1024px) !default;
$large-screen-min-width: em(1025px) !default;

$screen: "only screen" !default;
$small: "only screen and (min-width:#{$small-screen-min-width}) and (max-width:#{$small-screen-max-width})" !default;
$medium: "only screen and (min-width:#{$medium-screen-min-width}) and (max-width:#{$medium-screen-max-width})" !default;
$large: "only screen and (min-width:#{$large-screen-min-width})" !default;
$landscape: " and (orientation: landscape)" !default;
$portrait: " and (orientation: portrait)" !default;

And created a map that contains the breakpoints that we are interested in:

$screen: "only screen" !default;
$landscape: " and (orientation: landscape)" !default;
$portrait: " and (orientation: portrait)" !default;

$media-query-sizes: (
  small: (
    min: em(320px),
    max: em(767px)
  ),
  medium: (
    min: em(768px),
    max: em(1024px)
  ),
  large: (
    min: em(1025px)
  )
);

We have created a function that will create the media label based on the variables that have been given:

@function media-label($media, $orientation: false) {
  @if(not map-has-key($media-query-sizes, $media)){
    @warn "the $media value needs to be one of the following #{map-keys($media-query-sizes)}";
    @return false;
  }

  $media-sizes: map-get($media-query-sizes, $media);

  $media-label: $screen + " and (min-width:#{map-get($media-sizes, 'min')})";

  @if(length($media-sizes) > 1) {
   $media-label: $media-label +  " and (max-width:#{map-get($media-sizes, 'max')})";
  }

  @if $orientation {
    @if $orientation == landscape {
      $media-label: $media-label + $landscape;
    } @else {
      $media-label: $media-label + $portrait;
    }
  }

  @return $media-label;
}

This will allow us to re0use this logic in any area that is needed. Our current need is in out respond-to mixin:

@mixin respond-to($media, $orientation: false) {
  $media-query-label: media-label($media, $orientation);

  @if $media-query-label {
    @media #{media-label($media, $orientation)} {
      @content
    }
  }
}

Now we use this mixin wherever we need to add a media query in the following manner:

@include respond-to (small) {
  body {
    background-color: red;
  }
}
@include respond-to (small, landscape) {
  body {
    background-color: blue;
  }
}

@include respond-to (medium) {
  body {
    background-color: green;
  }
}

@include respond-to (medium, landscape) {
  body {
    background-color: yellow;
  }
}

@include respond-to (large) {
  body {
    background-color: black;
  }
}

@include respond-to (gubliguke) {// this one will error out and will not be displayed
  body {
    background-color: black;
  }
}

The above Sass will compile into the following CSS:

@media only screen and (min-width: 20em) and (max-width: 47.9375em) {
  body {
    background-color: red;
  }
}
@media only screen and (min-width: 20em) and (max-width: 47.9375em) and (orientation: landscape) {
  body {
    background-color: blue;
  }
}
@media only screen and (min-width: 48em) and (max-width: 64em) {
  body {
    background-color: green;
  }
}
@media only screen and (min-width: 48em) and (max-width: 64em) and (orientation: landscape) {
  body {
    background-color: yellow;
  }
}
@media only screen and (min-width: 64.0625em) {
  body {
    background-color: black;
  }
}

Functions and mixins are the backbone of Sass development and are extremely useful in promoting DRY Sass. Use them wisely and often.