A List Apart

Menu
Issue № 394

DRY-ing Out Your Sass Mixins

by Published in Code, CSS · 26 Comments

One of the most powerful features of the CSS preprocessor Sass is the mixin, an abstraction of a common pattern into a semantic and reusable chunk. Think of taking the styles for a button and, instead of needing to remember what all of the properties are, having a selector include the styles for the button instead. The button styles are maintained in a single place, making them easy to update and keep consistent.

But far too often, mixins are written in such a way that duplicates properties, creating multiple points of failure in our output CSS and bloating the size of the file. Now, if you’re using Sass, you’re already on your way to creating leaner CSS than the average programmer. But if you’re as obsessed with performance as I am, any bloat is unacceptable. In this article, I’ll walk through how to take your Sass mixins to the next ultra-light level of CSS output.

Some background on Sass

Sass acts as a layer between an authored stylesheet and the generated .css file that gets served to the browser, and adds many great features to help make writing and maintaining CSS easier. One of the things Sass lets stylesheet authors do is DRY out their CSS, making it easier to maintain. DRY, or “don’t repeat yourself,” is a programming principle coined in The Pragmatic Programmer that states:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

CSS is not very good at DRY; common sets of properties get duplicated all the time by necessity (think buttons). If you were to create styles for a button with three types, those styles would either need to be repeated for each button type, or the properties that make up that button would need to be split across multiple selectors. This puts authors between a rock and a hard place. Rewriting the properties for each type means lots of copying and pasting of CSS (increasing file size), multiple points of failure, no single source of truth, and a very fragile component in the end. On the other hand, splitting the properties into multiple selectors means there is no single, authoritative representation of any one type of button in a system, each instead being scattered between two or more selectors. This adds fragility to our components, as they are now ambiguously defined.

Ideally, what we want is a way to define the core styles in a single place (without duplication), and a single selector with which the styles can be applied.

Why DRY?

Why go through this trouble? In short, because DRY CSS can improve our site’s performance. When architecting a site, performance matters—from the image formats we choose to how we write our CSS selectors. This is especially true when talking about mobile, where something as basic to the web as an HTTP request can pose performance challenges. The long-held assumption that CSS file size doesn’t matter in the large scale of web performance doesn’t hold true when mobile users are faced with less than 100MB for total shared website cache. Every little bit of room that can be squeezed out of the cache counts.

The goal, then, is to create selectors that are maintainable in our Sass and our HTML, and whose CSS representation is as slim as possible to reduce its footprint in the cache.

Mixins and extends: two half-solutions

Sass’s mixins provide the answer to one of these problems: they allow stylesheet authors to create a single place where core styles can be defined and referenced. Mixins can even take arguments, allowing for slight changes from one mixin call to another and enabling different types of the same pattern to be created. There is, however, a problem with just utilizing mixins: if given the chance, mixins will write out their properties each time they are called, bloating the output CSS. Mixins solve the single, authoritative representation part of DRY-ing out the Sass declarations, but often leave duplicated properties in the output CSS.

Sass introduces another concept that can help to DRY out our CSS: extends. Used through the @extend directive, extends allow a stylesheet author to say, “I want selector A to be styled like selector B.” What Sass then does is comma-separate selector A with selector B, allowing them to share selector B’s properties, and write the remaining properties like normal. Unlike mixins, extends cannot be given arguments; it is an all-or-nothing deal.

Sass

.couch {
	padding: 2em;
	height: 37in;
	width: 88in;
	z-index: 40;  
}
  
.couch-leather {
	@extend .couch;
	background: saddlebrown;
}

.couch-fabric {
	@extend .couch;
	background: linen;
}

CSS

.couch, 
.couch-leather, 
.couch-fabric {
	padding: 2em;
	height: 37in;
	width: 88in;
	z-index: 40;
}

.couch-leather {
	background: saddlebrown;
}

.couch-fabric {
	background: linen;
}

Extends solve the duplicated property and single selector problem in the output CSS, but stylesheet authors still must maintain two separate sets of styles in their Sass, and need to remember which properties need to be added to each component type—as if they had written two selectors to begin with.

Mixins and extends both solve half of the problem apiece. By combining mixins and extends with some creative architecture and a few interesting features of Sass, a truly DRY mixin can be created that will combine both halves into a single, unambiguous, authoritative representation—both in the way a pattern is used and maintained in Sass and in how the styles of a component are applied and represented in the output CSS.

DRY building blocks

Four features of Sass comprise the cornerstone of building DRY mixins: placeholder selectors, map data types, the @at-root directive, and the unique-id() function.

Placeholder selectors

Placeholder selectors are a unique kind of selector for use with Sass’s @extend directive. Written like a class, but starting with a % instead of a ., they behave just like a normal extend except that they won’t get printed to the stylesheet unless extended. Just like normal extends, the selector gets placed in the stylesheet where the placeholder is declared.

Sass

%foo {
	color: red;
}

.bar {
	@extend %foo;
	background: blue;
}

.baz {
	@extend %foo;
	background: yellow;
}

CSS

.bar, 
.baz {
	color: red;
}

.bar {
	background: blue;
}

.baz {
	background: yellow;
}

Maps

Maps are a data type (like numbers, strings, and lists) in Sass 3.3 that behave in a similar way to objects in JavaScript. They are comprised of key/value pairs, where keys and values can be any of Sass’s data types (including maps themselves). Keys are always unique and can be retrieved by name, making them ideal for unique storage and retrieval.

Sass

$properties: (
	background: red,
	color: blue,
	font-size: 1em,
	font-family: (Helvetica, arial, sans-serif)
);

.foo {
	color: map-get($properties, color);
}

At-root

The @at-root directive, new to Sass 3.3, places contained definitions at the root of the stylesheet, regardless of current nesting.

Unique ID

The unique-id() function in Sass 3.3 returns a CSS identifier guaranteed to be unique to the current run of Sass.

Creating a basic mixin

Turning a pattern into a mixin requires looking to the core of the styles that make it up and determining what is shared and what comes from user input. For our purposes, let’s use a basic button as an example:

Sass

.button {
	background-color: #b4d455;
	border: 1px solid mix(black, #b4d455, 25%);
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
		background-color: mix(black, #b4d455, 15%);
		border-color: mix(black, #b4d455, 40%);
	}
}

To turn this into a mixin, choose which properties are user-controlled (dynamic) and which are not (static). Dynamic properties will be controlled by arguments passed into the mixin, while static properties will simply be written out. For our button, we only want color to be dynamic. Then we can call the mixin with our argument, and our CSS will be printed out as expected:

Sass

@mixin button($color) {
	background-color: $color;
	border: 1px solid mix(black, $color, 25%);
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

.button {
	@include button(#b4d455);
}

This works well, but this will produce lots of duplicate properties. Say we want to create a new color variation of our button. Our Sass (not including the mixin definition) and output CSS would look like the following:

Sass

.button-badass {
	@include button(#b4d455);
}

.button-coffee {
	@include button(#c0ffee);
}

CSS

.button-badass {
	background-color: #b4d455;
	border: 1px solid #879f3f;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-badass:hover {
	cursor: pointer;
	background-color: #99b448;
	border-color: #6c7f33;
}

.button-coffee {
	background-color: #c0ffee;
	border: 1px solid #90bfb2;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-coffee:hover {
	cursor: pointer;
	background-color: #a3d8ca;
	border-color: #73998e;
}

There are a bunch of duplicated properties in there, creating bloat in our output CSS. We don’t want that! This is where the creative use of placeholder selectors comes in.

DRY-ing out a mixin

DRY-ing out a mixin means splitting it into static and dynamic parts. The dynamic mixin is the one the user is going to call, and the static mixin is only going to contain the pieces that would otherwise get duplicated.

Sass

@mixin button($color) {
		@include button-static;

	background-color: $color;
	border-color: mix(black, $color, 25%);
  
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static {
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

Now that we have our mixin broken up into two parts, we want to extend the items in button-static to prevent duplication. We could do this by using a placeholder selector instead of a mixin, but that means the selectors will be moved in our stylesheet. Instead, we want to create a placeholder dynamically in place, so that it gets created the first time the selector is needed and retains the source order we expect. To do this, our first step is to create a global variable to hold the names of our dynamic selectors.

Sass

$Placeholder-Selectors: ();

Next, in button-static, we check to see if a key exists for our selector. We will call this key “button” for now. Using the map-get function, we will either get back the value of our key, or we will get back null if the key does not exist. If the key does not exist, we will set it to the value of a unique ID using map-merge. We use the !global flag, since we want to write to a global variable.

Sass

$Placeholder-Selectors: ();
// ...
@mixin button-static {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $button-selector == null {
		$button-selector: unique-id();
		$Placeholder-Selectors: map-merge($Placeholder-Selectors, ('button': $button-selector)) !global;
	}
	
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

Once we have determined whether an ID for our placeholder already exists, we need to create our placeholder. We do so with the @at-root directive and interpolation #{} to create a placeholder selector at the root of our directory with the name of our unique ID. The contents of that placeholder selector will be a call to our static mixin (recursive mixins, oh my!). We then extend that same placeholder selector, activating it and writing the properties to our CSS.

By using a placeholder selector here instead of extending a full selector like a class, these contents will only be included if the selector gets extended, thus reducing our output CSS. By using an extension here instead of writing out the properties, we also avoid duplicating properties. This, in turn, reduces fragility in our output CSS: every time this mixin gets called, these shared properties are actually shared in the output CSS instead of being roughly tied together through the CSS preprocessing step.

Sass

$Placeholder-Selectors: ();
// ...
@mixin button-static {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	@if $button-selector == null {
		$button-selector: unique-id();
		$Placeholder-Selectors: map-merge($Placeholder-Selectors, ('button': $button-selector)) !global;
	  
		@at-root %#{$button-selector} {
			@include button-static;
		}
	}
	@extend %#{$button-selector};   
	
	
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

But wait, we’re not quite done yet. Right now, we are still going to get duplicated output, which is something we don’t want (and we’re going to get a selector extending itself, which we also don’t want). To prevent this, we add an argument to button-static to dictate whether to go through the extend process or not. We’ll add this to our dynamic mixin as well, and pass it through to our static mixin. In the end, we’ll have the following mixins:

Sass

$Placeholder-Selectors: ();

@mixin button($color, $extend: true) {
	@include button-static($extend);
	
	background-color: $color;
	border-color: mix(black, $color, 25%);
	
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static($extend: true) {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $extend == true {
		@if $button-selector == null {
			$button-selector: unique-id();
			$Placeholder-Selectors: map-merge($Placeholder-Selectors, ('button': $button-selector)) !global;
			
			@at-root %#{$button-selector} {
				@include button-static(false);
			}
		}
		@extend %#{$button-selector};
		}
		@else {
		border: 1px solid;
		border-radius: 5px;
		padding: .25em .5em;
		
		&:hover {
			cursor: pointer;
		}
	}
}

After all this effort, we have created a way to easily maintain our styling in Sass, provide a single selector in our HTML, and keep the total amount of CSS to a minimum. No matter how many times we include the button mixin, we will never duplicate our static properties.

The first time we use our mixin, our styles will be created in the CSS where the mixin was called, preserving our intended cascade and reducing fragility. And since we allow multiple calls to the same mixin, we can easily create and maintain variations in both our Sass and our HTML.

With this written, our original example mixin calls now produce the following CSS:

Sass

.button-badass {
	@include button(#b4d455);
}

.button-coffee {
	@include button(#c0ffee);
}

.button-decaff {
	@include button(#decaff);
}

CSS

.button-badass {
	background-color: #b4d455;
	border-color: #879f3f;
}
.button-badass, 
.button-coffee, 
.button-decaff {
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-badass:hover, 
.button-coffee:hover, 
.button-decaff:hover {
	cursor: pointer;
}
.button-badass:hover {
	background-color: #99b448;
	border-color: #6c7f33;
}

.button-coffee {
	background-color: #c0ffee;
	border-color: #90bfb2;
}
.button-coffee:hover {
	background-color: #a3d8ca;
	border-color: #73998e;
}

.button-decaff {
	background-color: #decaff;
	border-color: #a697bf;
}
.button-decaff:hover {
	background-color: #bcabd8;
	border-color: #857999;
}

Our static properties get comma-separated in place where they were defined, which makes debugging easier, preserves our source order, and reduces our output CSS file size—and only the properties that change get new selectors. Nice, DRY mixins!

Going further

Now, rewriting this same pattern over and over again for each mixin isn’t DRY at all; in fact, it’s quite WET (“write everything twice”—programmers are a silly bunch). We don’t want to do that. Instead, think about creating a mixin for the placeholder generation, so you can call that instead. Or, if using the Toolkit Sass extension (either through Bower or as a Compass extension), the dynamic-extend mixin can be used to find, create, and extend a dynamic placeholder. Simply pass it a string name to search, like “button.”

Sass

@import "toolkit";

@mixin button($color, $extend: true) {
	@include button-static($extend);
	
	background-color: $color;
	border-color: mix(black, $color, 25%);
	
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static($extend: true) {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $extend == true {
		@include dynamic-extend('button') {
			@include button-static(false);
		}
	}
	@else {
		border: 1px solid;
		border-radius: 5px;
		padding: .25em .5em;

		&:hover {
			cursor: pointer;
		}
	}
}

With this, you can DRY out your DRY mixin pattern, thus reducing your input Sass files as well as your output CSS files, and ensuring you are meta-programming at your very best.

About the Author

26 Reader Comments

Load Comments