HTML & CSS
Article

Scaling Values Across Breakpoints Using Sass

By James Steinbach

Stop me if you’ve been here before: you’re given a desktop comp for a site, then after you build it you get verbal feedback (or maybe another comp, if you’re lucky) for a ‘mobile view’. You then need to make the site responsive (including a couple of in-between breakpoints), but you dread the work of manually figuring out all values for the CSS properties that change measurably from small to large screens (font-sizes, margins, widths, etc). What if there were a way to automatically scale those values across all your breakpoints and generate the media queries you need?

That’s the tool we’re going to build today.

Managing Breakpoints

We’ll start with a Sass map to store our breakpoints. Note: this is designed for mobile-first (min-width) media queries, so key-value pairs should be ordered from small to large.

$breakpoints: (
  small: 320px,
  medium: 480px,
  large: 960px,
  xlarge: 1200px
);

I won’t explain exactly how the bp() mixin works, but you can check out the code here (read this post for a more detailed explanation of the breakpoint mixin).

@mixin bp($name) {
    @if not map-has-key($breakpoints, $name) {
        @warn "Invalid breakpoint `#{$name}`.";
    } @else {
        @if map-get($breakpoints, $name) {
            @media (min-width: map-get($breakpoints, $name)) {
                @content;
            }
        } @else {
            @content;
        }
    }
}

Helper Functions

I broke a couple parts of the code out of the main mixin so they could serve as reusable functions. They’re fairly simple, so we’ll look at them quickly.

Getting Breakpoint Values

The first function is just shorthand for map-get(). It takes a $breakpoints key and returns its measurement value.

@function get_bp($label) {
  @return map-get($breakpoints, $label);
}

Rounding Numbers

We also function that simply rounds numbers to two decimal places for us, nothing fancy here:

@function round-num($num) {
  @return round($num*100)/100;
}

Scaling Values

Now we need a mixin that does the hard work of spreading value changes out across our designated breakpoints. This mixin accepts five parameters (two of which are optional). $property is the name of the CSS property being changed. $value-start and $value-end are the beginning and end values for the property. $bp-start and $bp-end are set to the small and xlarge breakpoints by default, but you can use override those defaults with names of any other key in $breakpoints.

@mixin spread-value($property, $value-start, $value-end, $bp-start: small, $bp-end: xlarge) {

  @if type-of($value-start) != number or type-of($value-end) != number {
    @warn "Either $value-start or $value-end is not a number: `#{$value-start}` | `#{$value-end}`"
  } @else {
    #{$property}: #{$value-start};
    $value-distance: $value-end - $value-start;
    $bp-distance: get_bp($bp-end) - get_bp($bp-start);
    $bp-keys: map-keys($breakpoints);
    $bp-list: ();

    $i: index($bp-keys, $bp-start);
    @while $i <= length($bp-keys) and nth($bp-keys, $i) != $bp-end {
      $i: $i + 1;
      $bp-list: join($bp-list, nth($bp-keys, $i));
    }

    @each $key in $bp-list {
      $percentage: ( get-bp($key) - get_bp($bp-start) ) / $bp-distance;
      @include bp($key) {
        #{$property}: round-num( ( $value-distance * $percentage ) + $value-start );
      }
    }
  }
}

Validating Values

The first thing we get the mixin to do is make sure that $value-start and $value-end are numbers. Numerical measurements are the only things we can scale (no spreading font-faces or background-images!). We validate the two value parameters by making sure their type is a number. This Sass function will evaluate any unit of measurement as number, so values with px, em, %, etc, all correctly validate as numbers. If either $value-start or $value-end is not a number, we’ll throw a warning to the compiler.

@if type-of($value-start) != number or type-of($value-end) != number {
    @warn "Either $value-start or $value-end is not a number: `#{$value-start}` | `#{$value-end}`"
  } @else {
  // …
  }

Setting the Default

The next thing to do is set the default value. With mobile-first media queries, this is $value-start. No matter what else happens, we’ll print that property-value pair to the stylesheet:

#{$property}: #{$value-start};

Start Using Math

Now we’re ready to start calculating the distances we’ll need to spread out our values accurately. We’ll set $value-distance to the distance between $value-start and $value-end and $bp-distance to the distance between the values of $bp-start and $bp-end.

$value-distance: $value-end - $value-start;
    $bp-distance: get_bp($bp-end) - get_bp($bp-start);

Next, we’ll use map-keys() to create a list of all the breakpoint names. This will make it easier to figure out how many breakpoints we’ll need to calculate values for. We’ll also initialize an empty list $bp-list that we’ll use to store the keys of the needed breakpoints.

$bp-keys: map-keys($breakpoints);
    $bp-list: ();

The following @while loop starts $i at the $bp-start‘s position in the list of breakpoints. We’ll stop the loop on either of two conditions: if $i exceeds the length of the $breakpoints map, or if $i is the index the breakpoint specified as $bp-end. When this loop finishes running, $bp-list will be a list of all the breakpoints we need to calculate values for.

$i: index($bp-keys, $bp-start);
    @while $i <= length($bp-keys) and nth($bp-keys, $i) != $bp-end {
      $i: $i + 1;
      $bp-list: join($bp-list, nth($bp-keys, $i));
    }

We now need the mixin to work its way through that list, generating the media queries and values needed. The $percentage variable calculates a percentage marking how far between $bp-start and $bp-end our current breakpoint is. Then @include bp($key) {} creates a media query for the current breakpoint.

Inside the breakpoint, we declare the property with the scaled value. To get the scaled value, we multiply the $value-distance by the $percentage of our breakpoint’s distance, then add the beginning value. This gives us value changes that are proportional to the width of the current breakpoint. You’ll notice we’re wrapping the output in our round-num() function to keep things nicely rounded.

@each $key in $bp-list {
      $percentage: ( get-bp($key) - get_bp($bp-start) ) / $bp-distance;
      @include bp($key) {
        #{$property}: round-num( ( $value-distance * $percentage ) + $value-start );
      }
    }

Usage

To use this mixin, include it inside any selector:

p {
  @include spread-value(margin-bottom, .6em, 1.6em);
}

With the default breakpoints we have in our map, it will generate the following CSS:

p {
  margin-bottom: 0.6em;
}
@media (min-width: 480px) {
  p {
    margin-bottom: 0.78em;
  }
}
@media (min-width: 960px) {
  p {
    margin-bottom: 1.33em;
  }
}
@media (min-width: 1200px) {
  p {
    margin-bottom: 1.6em;
  }
}

You can also specify which breakpoint to start or end the scaling at:

h1 {
  @include spread-value(font-size, 1.6em, 2.6em, medium, xlarge);
}

That will generate the following CSS:

h1 {
  font-size: 1.6em;
}
@media (min-width: 960px) {
  h1 {
    font-size: 2.27em;
  }
}
@media (min-width: 1200px) {
  h1 {
    font-size: 2.6em;
  }
}

Conclusion

Now we’ve got a pretty useful mixin for proportionally increasing or decreasing values across all our breakpoints. This can cut down development time pretty handily. It also provides a much smoother experience for other developers and managers who sit around and resize your pages in their browser! It’s also totally customizable because it relies on only the breakpoints you choose.

If you’ve got some great use cases for this mixin, please share them in the comments!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • Bruno Seixas

    This is really great!
    I mean, really great value with these mixins!
    Really thanks =)

    For me next project with a RWD I shall give it a real try!

    • http://jamessteinbach.com/ James Steinbach

      Glad you’ve found this useful, Bruno!

      If I were changing a value only once in an RWD project, I would just use the bp() mixin, not spread-value().

      This spread-value() mixin actually won’t output *any* media query wrapped styles if you use the same breakpoint for $bp-start & $bp-end. Take a look at line 13 in the mixin’s code block above. It will only add breakpoint names to $bp-list while nth($bp-keys, $i) does not equal $bp-end. If $bp-start & $bp-end are the same, that @while loop will never run, nothing will be added to $bp-list, and no responsive code will be generated. I put a demo of this in Sassmeister for you to look at: http://sassmeister.com/gist/f6b9714c0b002011b09b

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Front-end, once a week, for free.