Writing responsive CSS can be tedious because the media query instruction is quite verbose. Especially if you have many breakpoints to take into account. The following is a simple example of responsive code:

h1 {
  font-size: 2rem;
  @media screen and (min-width: map-get($breakpoints, md)) {
    font-size: 3rem;
  }
  @media screen and (min-width: map-get($breakpoints, md)) {
    font-size: 4.5rem;
  }
}

There are many repeated characters in that code, it would nice if we could use a terse syntax such as:

h1 {
  @include responsive(font-size, (sm: 2rem, md: 3rem, lg: 4.5rem));
}

Fortunately, we can write a simple mixin to handle it:

@mixin responsive($property, $map) {
  $keys: map-keys($map);

  // Minimum size
  #{$property}: map-get($map, nth($keys, 1));

  @each $key, $value in $map {
    @media screen and (min-width: map-get($breakpoints, $key)) {
      #{$property}: $value;
    }
  }
}

This mixin assumes the following:

  • There is a $breakpoints map defined such as $breakpoints: (sm: 480px, md: 992px, lg: 1200px);
  • The breakpoints you pass as arguments are sorted from smallest to largest. For example, you can pass (sm: 2rem, md: 3rem, lg: 4.5rem) but not (lg: 4.5rem, md: 3rem, sm: 2rem)
  • The first map element corresponds to the minimum size. In this example, it means that the smallest font-size will be 2rem even if the screen is smaller than the sm breakpoint.
  • The last map element corresponds to the maximum size. In this example, it means that the largest font-size will be 4.5rem even if the screen is larger than the lg breakpoint.

Pushing it further

The helper we've built works well, it adjusts the sizes at the breakpoints like we expect it to. However, in between them, nothing changes so we have elements that are not optimally displayed for devices that fall between two breakpoints. It would be nice if some properties could be scaled smoothly between the breakpoints instead. For example, font sizes, margins and padding are good candidates. Jake Wilson wrote an excellent article explaining how he built a fluid responsive mixin  for typography which uses the same syntax.

I won't go over the implementation in this article, Jake does a great job of explaining it. However, I'd like to give you a slightly modified version of his mixin; I modified it to use a breakpoint map and added unit conversion so I can specify some sizes in rem units.

Here's the complete file which I use in all my projects:

/// @author Chris Eppstein
/// @source https://github.com/sass/sass/issues/533#issuecomment-21445094
@function convert($value, $unit) {
  $convertable-units: px rem;
  $conversion-factors: 1 1rem/10px;
  @if index($convertable-units, unit($value)) and index($convertable-units, $unit) {
    @return
      $value / nth($conversion-factors,
      index($convertable-units, unit($value))) * nth($conversion-factors, index($convertable-units, $unit));
  } @else {
    @error 'Cannot convert #{unit($value)} to #{$unit}';
  }
}

/// Calculate the definition of a line between two points
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function linear-interpolation($map) {
  $keys: map-keys($map);
  @if (length($keys) != 2) {
    @error 'linear-interpolation() $map must be exactly 2 values';
  }

  // Variables for readability
  $x1: nth($keys, 1);
  $x2: nth($keys, 2);
  $y1: map-get($map, $x1);
  $y2: map-get($map, $x2);

  // Unit used in unit conversions
  $unit: unit($y1);

  // The slope
  $m: convert($y2 - $y1, $unit) / convert($x2 - $x1, $unit);

  // The y-intercept
  $b: convert($y1, $unit) - $m * convert($x1, $unit);

  // Determine if the sign should be positive or negative
  $sign: '+';
  @if ($b < 0) {
    $sign: '-';
    $b: abs($b);
  }

  @return calc(#{$m*100}vw #{$sign} #{$b});
}

/// @author Jake Wilson <jake.e.wilson@gmail.com>
/// @source https://www.smashingmagazine.com/2017/05/fluid-responsive-typography-css-poly-fluid-sizing/
/// @modified by Robert Al-Romhein <robert.alromhein@gmail.com>
///   Modified to use our breakpoints map instead of fixed pixel sizes
@mixin responsive-fluid($property, $map) {

  $length: length(map-keys($map));
  @if ($length < 2) {
    @error 'responsive-fluid() $map requires at least values';
  }

  $keys: map-keys($map); // Minimum size
  #{$property}: map-get($map, nth($keys, 1)); // Interpolated size through breakpoints

  @for $i from 1 through ($length - 1) {
    @media screen and (min-width: map-get($breakpoints, nth($keys, $i))) {

      // Variables for readability
      $value1: map-get($map, nth($keys, $i));
      $value2: map-get($map, nth($keys, ($i + 1)));
      $breakpoint1: map-get($breakpoints, nth($keys, $i));
      $breakpoint2: map-get($breakpoints, nth($keys, ($i+1)));

      // If values are not equal, perform linear interpolation
      @if ($value1 != $value2) {
        #{$property}: $value1; // Fallback for browsers that don't support calc
        #{$property}: linear-interpolation((
          $breakpoint1: $value1,
          $breakpoint2: $value2
        ));
      } @else {
        #{$property}: $value1;
      }
    }
  }

  // Maximum size
  @media screen and (min-width: map-get($breakpoints, nth($keys, $length))) {
    #{$property}: map-get($map, nth($keys, $length));
  }
}

/// @author Robert Al-Romhein <robert.alromhein@gmail.com>
@mixin responsive($property, $map) {
  $keys: map-keys($map);

  // Minimum size
  #{$property}: map-get($map, nth($keys, 1));

  @each $key, $value in $map {
    @media screen and (min-width: map-get($breakpoints, $key)) {
      #{$property}: $value;
    }
  }
}

Unit conversion

I use a conversion factor of 1rem/10px because I set a base font-size of 62.5% in all my projects which allows me to work with rems in multiples of 10 (1.6rem = 16px). If you are using the default browser font-size of 16px, you can use a conversion factor of 1rem/16px instead.

You can use the mixins like this:

h1 {
  @include responsive(font-size, (sm: 2rem, md: 3rem, lg: 4.5rem));
  
  // OR 
  
  @include responsive-fluid(font-size, (sm: 2rem, md: 3rem, lg: 4.5rem));
}

The responsive mixin can handle any property and can be used in all your responsive code, whether it be display or colors or opacity. Whereas the responsive-fluid mixin can only be used for numeric values. I mostly use the latter for font-size, margin, padding and the former for everything else.