@extend all the things

Until recently I haven’t paid much attention to Sass’ @extend directive, simply because I’ve been doing “old school” OOCSS, where I add the appropriate classes to the elements I want to target. I’m changing my habits and I’ll tell you why.

The site I’m working on now is a complex, massive site, counting hundreds of thousands of articles in different categories, sorted and catalogued by author, location, category, and several different criteria. It’s an overwhelming undertaking to write all the markup and css from scratch, and I find that it’s much too easy to get lost in mixins, helper classes, grids and media queries. Something clever had to be done.

When my friends ask me what I do, I tell them I ‘make websites’

As it happens Sass has had a bit of a growth spurt lately, and the latest “pre” release really packs a punch. A feature that triggered a new line of DRY thinking in me is the placeholder selector. In all essence it’s not much different than having a helper class that you add to elements to give them certain qualities, but used in the right way you can really make development a breeze, while keeping your markup and styles uncluttered and easy to read.

This combined with the new mixin content blocks, variable arguments and a toolset of generic functions and mixins means that you can automate a whole bucket load of your usual grunt work through Sass’ functionality.

Well, that was boring. What does it do?

I figure it’s best to start on the easy side of things. Remember having @mixins for all kinds of things like box-sizing: border-box and common transitions? Or adding the class row to your grid containers and column or span-4 to your grid columns? I say, enough of that nonsense! With some easy techniques we can get rid of all that class and mixin clutter.

Let’s say you need certain elements to user border-box as box-sizing. This requires at least 2 browser prefixes and a generic property. Including these properties in every element that needs these qualities makes no sense, it’s unnecessary repetition and can easily be avoided. What I’d do is make a placeholder selector called %border-box, and do @extend %border-box on all the elements that require border-box.

// Placeholder selector:
%border-box {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

// Usage:
.featured { @extend %border-box; }
.product { @extend %border-box; }

Output:

.featured, .product {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

This technique certainly seems a whole lot more elegant than adding these three lines to every element that requires this property. It’s like an inverted @mixin.

Sass is a witch and should be burnt at the stake

The true magic happens when you have built up a set of tools to really leverage these opportunities. I’ve built a “breakpoint” @mixin where I can output media queries from a pair of column number counts, as this site’s breakpoints are built on how many columns the different sizes can handle.

In the site settings I create variables for everything I need to automate the media queries (we’ll get to that):

// Layout
$column-width:              55;                     // Width - gutter
$column-gutter:             5;                      // Gutter width
$breakpoints:               6, 12, 16, 22, 28;      // List of media query breakpoints

Things I know from the design sketches: Each column should be 55 pixels wide with a 5 pixels gutter. Everything below 6 columns wide should be “small” size with 100% width on all columns. Break points should happen at 6, 12, 16, 22 and 28 columns.

And then I use a @mixin and some handy functions to output the correct media queries:

@function _em($pixel) { @return ($pixel / (16 / 1em)); }
@function calcwidth($cols) { @return ($cols * ($column-width + $column-gutter) - $column-gutter); }
@mixin breakpoint($from, $to: false) {
    @if $from == 0 {
        @media (max-width: _em(calcwidth($to) + $column-gutter)) { @content; }
    } @else if $to {
        @media (min-width: _em(calcwidth($from) + $column-gutter)) 
        and (max-width: _em(calcwidth($to) + $column-gutter)) { @content; }
    } @else {
        @media (min-width: _em(calcwidth($from) + $column-gutter)) { @content; }
    }
}

Notice the @content in my @mixin breakpoints()? That’s unsurprisingly where all the media query specific styling goes.

So what if I have content I only want to show on small devices, and other content I only want to show on bigger devices? Behold:

%hide-on-small {
    @include breakpoint(0,nth($breakpoints,1)) { // From zero width to the first breakpoint in our list
        display: none;
    }
}
%only-on-small {
    @include breakpoint(nth($breakpoints,1)) { // From the first breakpoint in our list and up.
        display: none;
    }
}

.menu-small { @extend %only-on-small; }
.menu-big { @extend %hide-on-small; }

Output:

@media (max-width: 22.5em) {
  .menu-big {
    display: none; 
  } 
}
@media (min-width: 22.5em) {
  .menu-small {
    display: none; 
  } 
}

So, for every element we need to display only on small devices, we’ll @extend %only-on-small, and we’ll get the @media queries outputted all smooth like that. A lot of you will by now think “What the fuck is he doing? Shouldn’t all the media queries be defined together?”. I thought so too, but after extensive testing, I’ve found there’s insignificant performance downsides with doing it in context. It might add a line or a few in your styles, but it’ll save you a lot of hassle and your styles are infinitely more readable.

Grids, grids, grids, why are we still talking about grids?

Ok, here comes the tricky part. We’ve decided on a bunch of breakpoints that fits the design of the site. and we want limitations on row and columns sizes based on the list. For every site we’ll need manual adjustments, but setting up a base “ruleset” for how wide the columns are allowed to be makes sense.

%column {
    @extend %border-box; // We want our columns to use border-box box-sizing, so we'll @extend
    float: left; 
    display: block; 
    position: relative; 
    @include rem(margin-left, $column-gutter); // Convert size to rems with pixel fallbacks
    $i: 0;
    $from: 0;
    @each $to in $breakpoints { // Loop through our breakpoints
      $i: $i + 1;
      @if $i == 1 { // If first in list (smallest size)
        @include breakpoint(0,$to) {
            width: 100%;
            margin-left: 0;
        }
      } @else {
        @include breakpoint($from,$to) {
            @include rem(max-width, calcwidth($from));
        }
      }
      $from: $to;
    }
    @include breakpoint($from) { // If last in list, from here to infinity
        @include rem(max-width, calcwidth($from));
    }
}

.featured { @include columns(12); @extend %column; }
.sidebar { @include columns(6); @extend %column; }

Output:

.featured {
  width: 715px;
  width: 44.6875rem;
}
.sidebar {
  width: 355px;
  width: 22.1875rem; 
}
.featured, .sidebar {
  float: left;
  display: block;
  position: relative;
  margin-left: 5px;
  margin-left: 0.3125rem; }
  @media (max-width: 22.5em) {
    .featured, .sidebar {
      width: 100%;
      margin-left: 0; } }
  @media (min-width: 22.5em) and (max-width: 45em) {
    .featured, .sidebar {
      max-width: 355px;
      max-width: 22.1875rem; } }
  @media (min-width: 45em) and (max-width: 60em) {
    .featured, .sidebar {
      max-width: 715px;
      max-width: 44.6875rem; } }
  @media (min-width: 60em) and (max-width: 82.5em) {
    .featured, .sidebar {
      max-width: 955px;
      max-width: 59.6875rem; } }
  @media (min-width: 82.5em) and (max-width: 105em) {
    .featured, .sidebar {
      max-width: 1315px;
      max-width: 82.1875rem; } }
  @media (min-width: 105em) {
    .featured, .sidebar {
      max-width: 1675px;
      max-width: 104.6875rem; } }

The hope is that pretty much all of the media queries are attached to the row and column classes, and every thing else is taken care of by building the modules scalable so they can fit in whatever container they’re in. But individual adjustments are possible, as demonstrated above with the .hide-on-small example.

Screw you guys, I’m going home

There’s a bit of example overdose here, but it’d be unfortunate if I was to try to explain the technique without showing extensive samples. As you can see the possibilities are near endless for what you’ve always wished css could do.

Writing with Sass’ @extend instead of vanilla css helper classes has helped remove a lot of clutter from my HTML markup and I no longer worry about repeating myself too much. There’s still a way to go to write the perfect toolset but I’m getting there. Tweet me if you have any arguments for why what I’m doing is wrong, or just, you know, email me.

Comments? Tweet me!