Sass: Mixins

  • SCSS
  • Sass
  • CSS

A few weeks ago I wrote a blog post about Sass placeholders, which is one of the mechanisms that Sass gives us for managing repetition in a Sass library. In today's blog post, I'll write about Sass mixins, which is one of the other tools to help with repetitive work in Sass. At first glance, placeholders and mixins are very similar, as they both allow you to write Sass code in one place and easily use that code in multiple places. However, once you start digging in you quickly discover how they differ in their flexibility and how they compile. Let's dive in!

Example project setup

For this blog post I'll continue to build on the same project that I used for the past blog post.

Once more, here is our project structure:

sass-project├── node_modules├── package.json└── styles/    ├── axioms/    │   ├── _breakpoints.scss    │   ├── _directions.scss    │   ├── _space.scss    │   └── _typography.scss    ├── components/    │   └── _buttons.scss    ├── css/    ├── functions/    │   └── _pxToRem.scss    ├── patterns/    │   └── _margin.scss    └── styles.scss

The contents of my package.json file has not changed:

// package.json{  "name": "sass-project",  "version": "1.0.0",  "scripts": {    "sass": "sass styles/styles.scss:styles/css/styles.css"  },  "dependencies": {    "sass": "^1.83.4"  }}

styles/axioms/_space.scss and styles/axioms/_typography.scss have not changed:

/* styles/axioms/_space.scss */@use "sass:map";@use "../functions/pxToRem";
$size--100: 2px;$size--200: 4px;$size--300: 8px;$size--400: 16px;
$space-map: (  0: 0,  100: pxToRem.pxToRem($size--100),  200: pxToRem.pxToRem($size--200),  300: pxToRem.pxToRem($size--300),  400: pxToRem.pxToRem($size--400),);
@function Space($value) {  @return map.get($space-map, $value);}
/* styles/axioms/_typography.scss */$font-size--100: 11px;$line-height--100: 1.7;$font-size--200: 13px;$line-height--200: 1.64;$font-size--300: 16px;$line-height--300: 1.5;$font-size--400: 18px;$line-height--400: 1.48;
$font-size-base: $font-size--400;
@mixin f100 {  font-size: $font-size--100;  line-height: $line-height--100;}
@mixin f200 {  font-size: $font-size--200;  line-height: $line-height--200;}
@mixin f300 {  font-size: $font-size--300;  line-height: $line-height--300;}
@mixin f400 {  font-size: $font-size--400;  line-height: $line-height--400;}

I have added two new axioms files, styles/axioms/_breakpoints.scss and styles/axioms/_directions.scss, but those are empty for the moment.

I am keeping my styles/components folder, but I won't really be touching its contents in this blog post.

I also have a new folder, styles/patterns, and in that I have a _margin.scss file, where I will define some utility margin classes. For now, this is an empty file.

As always, my entry point is styles/styles.scss, and running the build command compiles everything to styles/css/styles.css. I'll start off with an empty styles/styles.scss file.

Generating CSS vendor prefixes

Most of the early tutorials and use cases focus on cutting down the repetitive work of managing CSS vendor prefixes:

@mixin border-radius($radius) {  -webkit-border-radius: $radius;  -moz-border-radius: $radius;  border-radius: $radius;}
.box {  @include border-radius(5px);}
.button {  @include border-radius(20px);}
.card {  @include border-radius(60px);}

I've defined a border-radius mixin, and using the $radius argument, I can pass different values in different contexts. The mixin will use the provided $radius value and plug it into a few different browser vendor versions of the border-radius CSS property, as well as the standard border-radius property. The above generates the following:

.box {  -webkit-border-radius: 5px;  -moz-border-radius: 5px;  border-radius: 5px;}
.button {  -webkit-border-radius: 20px;  -moz-border-radius: 20px;  border-radius: 20px;}
.card {  -webkit-border-radius: 60px;  -moz-border-radius: 60px;  border-radius: 60px;}

This isn't as common these days, since nearly every modern front-end framework includes some kind of post-CSS compilation step that will automatically prepend vendor prefixes so you don't have to worry about it. That said, I'm including it here because it illustrates the mental model that Sass Mixins help to reduce repetition.

Standard media query breakpoints

Let's move to a more modern and practical way to use Sass mixins. One of my favorite ways to use mixins is to produce media queries. On its own, writing out a plain media query isn't too bad:

@media screen and (min-width: 768px) {  /* styles here */}

But as your application grows this can get pretty repetitive. It's also common for a team of developers to work with designers to come up with "standard breakpoints", or established media query values that the designers design for and that developers build for. In both of these scenarios it can be extraordinarily helpful to define your standard breakpoints as Sass variables in one place, and to @use those variables or @include a mixin that uses those variables.

Breakpoint variables

Just like the typography and spacing constants, I want to put these breakpoint constants into an axioms file, so I'll add the following to styles/axioms/_breakpoints.scss:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;

We're going to build a mobile-first utility class system where styles are applied at all screen sizes and overridden at the above viewport widths. On its own these variables don't do anything, but we can interpolate them with longer variables like so:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;
$breakpoint-md: "screen and (min-width: #{$md-width})";$breakpoint-lg: "screen and (min-width: #{$lg-width})";$breakpoint-xl: "screen and (min-width: #{$xl-width})";

Right away this lets us simplify things elsewhere in our project.For example, say we are doing some layout work in our main styles/styles.scss file, and we want to adjust gutter sizes on a main element based on screen size. We can do so pretty easily by @useing our breakpoints file:

/* styles/styles.scss */@use "./axioms/breakpoints";
main {  padding: 18px;  @media #{breakpoints.$breakpoint-md} {    padding: 20px;  }  @media #{breakpoints.$breakpoint-lg} {    padding: 24px;  }  @media #{breakpoints.$breakpoint-xl} {    padding: 32px;  }}

It's a pretty basic implementation, but it helps to safeguard from having to write, remember, or look up the agreed-upon standard breakpoints. This compiles to the following:

/* css/styles.css */main {  padding: 18px;}@media screen and (min-width: 480px) {  main {    padding: 20px;  }}@media screen and (min-width: 769px) {  main {    padding: 24px;  }}@media screen and (min-width: 1025px) {  main {    padding: 32px;  }}

Breakpoint mixins

This is pretty good and I've worked on teams where this sort of thing scales nicely. That said, we can go a little bit further by using mixins. Let's go back to our styles/axioms/_breakpoints.scss file and write out some mixins:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;
$breakpoint-md: "screen and (min-width: #{$md-width})";$breakpoint-lg: "screen and (min-width: #{$lg-width})";$breakpoint-xl: "screen and (min-width: #{$xl-width})";
@mixin breakpoint-md {  @media #{$breakpoint-md} {    @content;  }}
@mixin breakpoint-lg {  @media #{$breakpoint-lg} {    @content;  }}
@mixin breakpoint-xl {  @media #{$breakpoint-xl} {    @content;  }}

The change we've made here is to define a mixin for each of the main breakpoints we want to use. Inside of those mixins we use the various respective breakpoint variables, and inside of that we use the @content directive. This will let us pass in whatever CSS declarations we want to the mixin.

Now let's use these mixins over in styles/styles.scss:

/* styles/styles.scss */@use "./axioms/breakpoints";
main {  padding: 18px;  @include breakpoints.breakpoint-md {    padding: 20px;  }  @include breakpoints.breakpoint-lg {    padding: 24px;  }  @include breakpoints.breakpoint-xl {    padding: 32px;  }}

To my eyes this is way cleaner than both writing media queries everywhere and just using variables. And it still compiles to what we want it to:

/* styles/css/styles.css */main {  padding: 18px;}@media screen and (min-width: 480px) {  main {    padding: 20px;  }}@media screen and (min-width: 769px) {  main {    padding: 24px;  }}@media screen and (min-width: 1025px) {  main {    padding: 32px;  }}

Breakpoint mixin using arguments

This is an improvement, but we can do better still. Mixins allow us to pass arguments. If we pass the our mixin the breakpoint we want to use, then we can further clean up our styles/axioms/_breakpoints.scss file:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;
$breakpoint-md: "screen and (min-width: #{$md-width})";$breakpoint-lg: "screen and (min-width: #{$lg-width})";$breakpoint-xl: "screen and (min-width: #{$xl-width})";
@mixin breakpoint($breakpoint) {  @if $breakpoint == "md" {    @media #{$breakpoint-md} {      @content;    }  } @else if $breakpoint == "lg" {    @media #{$breakpoint-lg} {      @content;    }  } @else if $breakpoint == "xl" {    @media #{$breakpoint-xl} {      @content;    }  } @else {    @error "Unknown breakpoint #{$breakpoint}";  }}

We pass in a $breakpoint argument, then pass it along to a set of @if / @else if / @else statements. We'll update things over in styles/styles.scss to use this new mixin:

/* styles/styles.scss */@use "./axioms/breakpoints";
main {  padding: 18px;  @include breakpoints.breakpoint("md") {    padding: 20px;  }  @include breakpoints.breakpoint("lg") {    padding: 24px;  }  @include breakpoints.breakpoint("xl") {    padding: 32px;  }}

Now we only have one mixin to manage, rather than three, and things still compile to what we want:

/* styles/css/styles.css */main {  padding: 18px;}@media screen and (min-width: 480px) {  main {    padding: 20px;  }}@media screen and (min-width: 769px) {  main {    padding: 24px;  }}@media screen and (min-width: 1025px) {  main {    padding: 32px;  }}

If you hate how @include breakpoints.breakpoint("md") looks, you can adjust the namespace using the as * wildcard in the @use statement:

/* styles/styles.scss */@use "./axioms/breakpoints" as *;
main {  padding: 18px;  @include breakpoint("md") {    padding: 20px;  }  @include breakpoint("lg") {    padding: 24px;  }  @include breakpoint("xl") {    padding: 32px;  }}

This further tidies things up, but I personally prefer to keep most namespaces verbose. See my Sass: @use and @forward blog post for more info and reasoning as to why.

You might have noticed this little block in styles/axioms/_breakpoints.scss:

/* styles/axioms/_breakpoints.scss */} @else {  @error "Unknown breakpoint #{$breakpoint}";}

This helps us ensure that the mixin we're using can't get misused. If an unrecognized argument is passed to the mixin, it will throw an error at compile time. For example:

/* styles/styles.scss */@use "./axioms/breakpoints";
main {  padding: 18px;  @include breakpoints.breakpoint("ganondorf") {    padding: 20px;  }}

If I try to compile this, it will throw an error:

Error: "Unknown breakpoint ganondorf"5 │   @include breakpoints.breakpoint("ganondorf") {  │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  styles/styles.scss 5:3  root stylesheet

Writing utility styles

Aside from helping library authors reduce the amount of repetitive code they have to write, Sass mixins are also incredibly useful to do the repetitive work of crafting sets of utility style classes.

Say we are creating our own Bootstrap-style CSS library that uses a set of spacing tokens, defined in styles/axioms/_space.scss:

/* styles/axioms/_space.scss */@use "sass:map";@use "../functions/pxToRem";
$size--100: 2px;$size--200: 4px;$size--300: 8px;$size--400: 16px;
$space-map: (  0: 0,  100: pxToRem.pxToRem($size--100),  200: pxToRem.pxToRem($size--200),  300: pxToRem.pxToRem($size--300),  400: pxToRem.pxToRem($size--400),);
@function Space($value) {  @return map.get($space-map, $value);}

For each of the size tokens, we are converting them from pixel values to REM values using our pxToRem function. We make those tokens available within $space-map, and offer a Space function that will essentially serve as a shorthand API for all of the values in that function.

Creating a margin utility class system

What I want to do at this point is generate utility classes that look like the following:

.margin0 {  margin: 0;}
.margin100 {  margin: 0.1111111111rem;}
.margin200 {  margin: 0.2222222222rem;}
.margin300 {  margin: 0.4444444444rem;}
.margin400 {  margin: 0.8888888889rem;}

We are crafting class names that utilize each of our tokens and their associated token values for the margin CSS rule within that class. It's exactly the kind of repetitive work that mixins make very easy for us. I'll head over to styles/patterns/_margin.scss and add the following:

/* styles/patterns/_margin.scss */@use "../axioms/space";
@mixin margin() {  @each $token, $size in space.$space-map {    .margin#{$token} {      margin: $size;    }  }}
@include margin;

We are @useing our space axiom, and within our margin mixin, looping over the $space-map and interpolating the $token into the class name and passing the $size variable value to the CSS margin rule. At the end of the file we @include the mixin.

Over in styles/styles.scss, we need to @use the styles/patterns/_margin.scss file:

/* styles/styles.scss */@use "./patterns/margin";

With that in place, if we compile our CSS, here is the result:

/* styles/css/styles.css */.margin0 {  margin: 0;}
.margin100 {  margin: 0.1111111111rem;}
.margin200 {  margin: 0.2222222222rem;}
.margin300 {  margin: 0.4444444444rem;}
.margin400 {  margin: 0.8888888889rem;}

This is exactly what we want.

Adding our breakpoints

On its own this is great, but we can combine this work with the work we did earlier to create these classes for each breakpoint. That would look something like this:

.margin0 {  margin: 0;}.margin100 {  margin: 0.1111111111rem;}/* .margin200, .margin300, and .margin400 continue here */
@media screen and (min-width: 480px) {  .margin0-md {    margin: 0;  }  .margin100-md {    margin: 0.1111111111rem;  }  /* .margin200-md, .margin300-md, and .margin400-md continue here */}
@media screen and (min-width: 769px) {  .margin0-lg {    margin: 0;  }  .margin100-lg {    margin: 0.1111111111rem;  }  /* .margin200-lg, .margin300-lg, and .margin400-lg continue here */}@media screen and (min-width: 1025px) {  .margin0-xl {    margin: 0;  }  .margin100-xl {    margin: 0.1111111111rem;  }  /* .margin200-xl, .margin300-xl, and .margin400-xl continue here */}

To accommodate these kinds of changes, we can use a lot of what we've written already (including our breakpoint mixin from earlier), but we'll need to make several changes to styles/axioms/_breakpoints.scss and styles/patterns/_margin.scss.

Starting in _breakpoints.scss, it will be very helpful to have a $breakpoints-map to loop over, so lets add that:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;
$breakpoint-md: "screen and (min-width: #{$md-width})";$breakpoint-lg: "screen and (min-width: #{$lg-width})";$breakpoint-xl: "screen and (min-width: #{$xl-width})";
@mixin breakpoint($breakpoint) {  @if $breakpoint == "md" {    @media #{$breakpoint-md} {      @content;    }  } @else if $breakpoint == "lg" {    @media #{$breakpoint-lg} {      @content;    }  } @else if $breakpoint == "xl" {    @media #{$breakpoint-xl} {      @content;    }  } @else {    @error "Unknown breakpoint #{$breakpoint}";  }}
$breakpoints-map: ("-sm", "-md", "-lg", "-xl");

We'll eventually be passing the values in $breakpoints-map, like "-md", "-lg", and "-xl" (with hyphens) to the breakpoint mixin, so that mixin should accommodate both hyphenated and unhyphenated $breakpoint arguments:

/* styles/axioms/_breakpoints.scss */$md-width: 480px;$lg-width: 769px;$xl-width: 1025px;
$breakpoint-md: "screen and (min-width: #{$md-width})";$breakpoint-lg: "screen and (min-width: #{$lg-width})";$breakpoint-xl: "screen and (min-width: #{$xl-width})";
@mixin breakpoint($breakpoint) {  @if $breakpoint == "md" or $breakpoint == "-md" {    @media #{$breakpoint-md} {      @content;    }  } @else if $breakpoint == "lg" or $breakpoint == "-lg" {    @media #{$breakpoint-lg} {      @content;    }  } @else if $breakpoint == "xl" or $breakpoint == "-xl" {    @media #{$breakpoint-xl} {      @content;    }  } @else {    @error "Unknown breakpoint #{$breakpoint}";  }}
$breakpoints-map: ("-sm", "-md", "-lg", "-xl");

With that in place, we can start @useing the breakpoints axiom and leveraging the $breakpoints-map like so:

/* styles/patterns/_margin.scss */@use "../axioms/breakpoints";@use "../axioms/space";
@mixin margin($breakpoint-modifier: "") {  @each $token, $size in space.$space-map {    .margin#{$token}#{$breakpoint-modifier} {      margin: $size;    }  }}
@each $breakpoint-modifier in breakpoints.$breakpoints-map {  @if ($breakpoint-modifier == "-sm") {    @include margin;  } @else {    @include breakpoints.breakpoint($breakpoint-modifier) {      @include margin($breakpoint-modifier);    }  }}

I've adjusted the margin mixin to accept a $breakpoint-modifier argument, with a default empty string value. I am interpolating that $breakpoint-modifier into the classname after the $token interpolation.

Below that, I'm looping over breakpoints.$breakpoints-map. If the $breakpoint-modifier is "-sm", then I don't want to append that modifier, and I want to keep the classname as .ma0, .ma100, etc. I have a conditional to check that $breakpoint-modifier and @include margin without any argument if that is the case:

/* styles/patterns/_margin.scss */@if ($breakpoint-modifier == "-sm") {  @include margin;}

Otherwise, $breakpoint-modifier is -md, -lg, or -xl, and I do want to include that modifier in the classname. But I also want to apply the styles within the appropriate media query. To do so, I call @include margin($breakpoint-modifier); within @include breakpoints.breakpoint($breakpoint-modifier) { and our breakpoint mixin we wrote earlier will work its magic:

/* styles/patterns/_margin.scss */@else {  @include breakpoints.breakpoint($breakpoint-modifier) {    @include margin($breakpoint-modifier);  }}

With that in place, here is our compiled CSS:

/* styles/css/styles.css */.margin0 {  margin: 0;}
.margin100 {  margin: 0.1111111111rem;}
.margin200 {  margin: 0.2222222222rem;}
.margin300 {  margin: 0.4444444444rem;}
.margin400 {  margin: 0.8888888889rem;}
@media screen and (min-width: 480px) {  .margin0-md {    margin: 0;  }  .margin100-md {    margin: 0.1111111111rem;  }  .margin200-md {    margin: 0.2222222222rem;  }  .margin300-md {    margin: 0.4444444444rem;  }  .margin400-md {    margin: 0.8888888889rem;  }}@media screen and (min-width: 769px) {  .margin0-lg {    margin: 0;  }  .margin100-lg {    margin: 0.1111111111rem;  }  .margin200-lg {    margin: 0.2222222222rem;  }  .margin300-lg {    margin: 0.4444444444rem;  }  .margin400-lg {    margin: 0.8888888889rem;  }}@media screen and (min-width: 1025px) {  .margin0-xl {    margin: 0;  }  .margin100-xl {    margin: 0.1111111111rem;  }  .margin200-xl {    margin: 0.2222222222rem;  }  .margin300-xl {    margin: 0.4444444444rem;  }  .margin400-xl {    margin: 0.8888888889rem;  }}

Adding directions

This is cool, but our utility classes are only applying margin token values to all sizes of the CSS box. I want to be able to use classes to add just margin-top, margin-right, margin-bottom, or margin-left in isolation. Rather than have something like .margin100, the goal is to have something like this:

.mt100 {  margin-top: 0.1111111111rem;}@media screen and (min-width: 480px) {  .mt100-md {    margin-top: 0.1111111111rem;  }}@media screen and (min-width: 769px) {  .mt100-lg {    margin-top: 0.1111111111rem;  }}@media screen and (min-width: 1025px) {  .mt100-xl {    margin-top: 0.1111111111rem;  }}

We want to repeat this pattern for every space token, every direction, and every breakpoint. It's a similar pattern to what we've just done. We'll head over to styles/axioms/_directions.scss and add the following:

$cardinal-directions-map: (  t: top,  r: right,  b: bottom,  l: left,);

Now we'll update styles/patterns/_margin.scss to the following:

@use "../axioms/breakpoints";@use "../axioms/directions";@use "../axioms/space";
@mixin margin($breakpoint-modifier: "") {  @each $direction-modifier, $direction in directions.$cardinal-directions-map {    @each $token, $size in space.$space-map {      .m#{$direction-modifier}#{$token}#{$breakpoint-modifier} {        margin-#{$direction}: $size;      }    }  }}
@each $breakpoint-modifier in breakpoints.$breakpoints-map {  @if ($breakpoint-modifier == "-sm") {    @include margin;  } @else {    @include breakpoints.breakpoint($breakpoint-modifier) {      @include margin($breakpoint-modifier);    }  }}

Aside from @useing the directions axiom, I've modified the margin mixin to loop over directions.$cardinal-directions-map, creating $direction-modifier and $direction variables. I interpolate the $direction-modifier into the classname, creating .mt100, .mr100, mb100, and so on. I also interpolate $direction into the rule name. This results in four directions for every token, all repeated for every breakpoint. Our compiled CSS looks something like the following (abbreviated because it's starting to get very long):

.mt0 {  margin-top: 0;}
.mt100 {  margin-top: 0.1111111111rem;}
/* ...etc... */
.mr0 {  margin-right: 0;}
.mr100 {  margin-right: 0.1111111111rem;}
/* ...etc... */
.mb0 {  margin-bottom: 0;}
.mb100 {  margin-bottom: 0.1111111111rem;}
/* ...etc... */
.ml0 {  margin-left: 0;}
.ml100 {  margin-left: 0.1111111111rem;}
/* ...etc... */
@media screen and (min-width: 480px) {  .mt0-md {    margin-top: 0;  }  .mt100-md {    margin-top: 0.1111111111rem;  }  /* ...etc... */  .mr0-md {    margin-right: 0;  }  .mr100-md {    margin-right: 0.1111111111rem;  }  /* ...etc... */}@media screen and (min-width: 769px) {  .mt0-lg {    margin-top: 0;  }  .mt100-lg {    margin-top: 0.1111111111rem;  }  /* ...etc... */  .mr0-lg {    margin-right: 0;  }  .mr100-lg {    margin-right: 0.1111111111rem;  }  /* ...etc... */}

Adding compound directions

This is cool, but we've lost out on our general margin-all classes. It would be ideal if, in addition to mt, mr, ml, and mb, we had something like ma, which would be shorthand for margin-all, mx, which would be shorthand for margin-left and margin-right, and my, which would be shorthand for margin-top and margin-bottom. We can add to the work we've done above to achieve this.

Let's add a new $compound-directions-map over in styles/axioms/_directions.scss:

$cardinal-directions-map: (  t: top,  r: right,  b: bottom,  l: left,);
$compound-directions-map: (  a: all,  x: x,  y: y,);

Similar to how we added conditional logic flow in our breakpoint mixin, we can update our margin mixin to use these new compound directions:

/* styles/patterns/_margin.scss */@use "sass:map";@use "../axioms/breakpoints";@use "../axioms/directions";@use "../axioms/space";
@mixin margin($breakpoint-modifier: "") {  @each $direction-modifier,    $direction      in map.merge(        directions.$compound-directions-map,        directions.$cardinal-directions-map      )  {    @each $token, $size in space.$space-map {      @if ($direction == all) {        .m#{$direction-modifier}#{$token}#{$breakpoint-modifier} {          margin: $size;        }      } @else if ($direction == x) {        .m#{$direction-modifier}#{$token}#{$breakpoint-modifier} {          margin-right: $size;          margin-left: $size;        }      } @else if ($direction == y) {        .m#{$direction-modifier}#{$token}#{$breakpoint-modifier} {          margin-top: $size;          margin-bottom: $size;        }      } @else {        .m#{$direction-modifier}#{$token}#{$breakpoint-modifier} {          margin-#{$direction}: $size;        }      }    }  }}
@each $breakpoint-modifier in breakpoints.$breakpoints-map {  @if ($breakpoint-modifier == "-sm") {    @include margin;  } @else {    @include breakpoints.breakpoint($breakpoint-modifier) {      @include margin($breakpoint-modifier);    }  }}

We have to @use "sass:map"; in order to map.merge() the $cardinal-directions-map and $compound-directions-map into a single map to loop over. You may notice that I have $compound-directions-map first, followed by $cardinal-directions-map. The order here is crucial, because I want the shorthand ordinal directions to compile first, and the single directions (top, right, bottom, and left) to come second. This gives us the power to use the shorthand utility classes and overwrite a single direction as needed.

With the above changes in place, our compiled CSS looks something like this (once again, abbreviated):

/* styles/css/styles.css */.ma0 {  margin: 0;}
.ma100 {  margin: 0.1111111111rem;}
/* etc....ma200, .ma300, and .ma400 */
.mx0 {  margin-right: 0;  margin-left: 0;}
.mx100 {  margin-right: 0.1111111111rem;  margin-left: 0.1111111111rem;}
/* etc....mx200, .mx300, and .mx400 */
.my0 {  margin-top: 0;  margin-bottom: 0;}
.my100 {  margin-top: 0.1111111111rem;  margin-bottom: 0.1111111111rem;}
/* etc....my200, .my300, and .my400 */
.mt0 {  margin-top: 0;}
.mt100 {  margin-top: 0.1111111111rem;}
/* etc....mt200, .mt300, and .mt400 */
.mr0 {  margin-right: 0;}
.mr100 {  margin-right: 0.1111111111rem;}
/* etc....mr200, .mr300, and .mr400 */
.mb0 {  margin-bottom: 0;}
.mb100 {  margin-bottom: 0.1111111111rem;}
/* etc....mb200, .mb300, and .mb400 */
.ml0 {  margin-left: 0;}
.ml100 {  margin-left: 0.1111111111rem;}
/* etc....ml200, .ml300, and .ml400 */
@media screen and (min-width: 480px) {  .ma0-md {    margin: 0;  }  .ma100-md {    margin: 0.1111111111rem;  }  /* etc...repeat all space tokens for all directions for this breakpoint */}
/* etc...repeat all space tokens for all directions for all breakpoints */

Summary

  • Mixins are a powerful tool for producing repetitive CSS programmatically
  • You can "write once, use everywhere" for things like CSS vendor prefixes or media queries
  • You can leverage mixin arguments, the @children directive, and Sass's flow control rules (@each, @if, @else, etc.) to succinctly create a system of CSS utility classes

Mixins vs. Placeholders

There is a lot of overlap between mixins and placeholders as they both let you reuse styles in a Sass library, but they differ in how and when you use them:

  • Placeholders:
    • get @extended
    • are more strict and a lot less flexible than mixins, since you can't use @content, pass arguments, or use a placeholder within a media query
    • Always compile to the same thing, no matter where they're @extended
    • make it easy for shared styles to get grouped together into comma-separated selector lists at compile time, resulting in the leanest possible CSS for shared styles
    • benefit Sass library authors by offering extensible styles that only compile if they are @extendded
  • Mixins:
    • get @included
    • are more flexible than placeholders, since you can use @content, pass arguments, and use a mixin within a media query
    • do not group duplicate style rulesets into comma-separated selector lists, resulting in longer, more robust CSS
    • benefit Sass library authors by
      • offering ways to write shorter Sass that compiles to lengthier, more repetitive CSS; i.e., vendor prefixes or media queries
      • enabling logic to create full utility class systems programmatically
    • Anything that @includes a mixin will copy the contents of that mixin wherever that mixin is @included, not necessarily grouped; so while this can offer more flexibility, it can lead to a larger bundled CSS file.

I like to think of it like this:

  • Placeholders are less flexible, but that lack of flexibility results in much leaner CSS; mixins are far more flexible, but that flexibility comes with the tradeoff of longer compiled CSS
  • Placeholders let you make a lot of CSS styles available to someone using your library, opting into using only the styles they need; mixins let you produce a lot of production CSS styles, but you would probably want to have some kind of a CSS purge step to make sure you're only shipping what's actually necessary.
  • Placeholders work nicely with component libraries (where you want to style all buttons similarly, all form inputs similarly, and so on); mixins work nicely with utility class CSS libraries, where you have a system of tokens applied to specific CSS properties