Sass: Mixins
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 @use
ing 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 @use
ing 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 @use
ing 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 @use
ing 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
@extend
ed - 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
@extend
ded
- get
- 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
@include
s a mixin will copy the contents of that mixin wherever that mixin is@include
d, not necessarily grouped; so while this can offer more flexibility, it can lead to a larger bundled CSS file.
- get
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