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!
James is a Senior Front-End Developer with almost 10 years total experience in freelance, contract, and agency work. He has spoken at conferences, including local WordPress meet-ups and the online WP Summit. James's favorite parts of web development include creating meaningful animations, presenting unique responsive design solutions, and pushing Sass’s limits to write powerful modular CSS. You can find out more about James at jamessteinbach.com.