Twitter Bootstrap: Custom Class names using Sass

Update: The instructions in this post were for Bootstrap 2.x, and are very likely outdated.

There’s no question why Twitter Bootstrap is the most watched repo on Github. It’s amazing - saving developers everywhere from reinventing wheels and giving them the time they need to work on features that apply specifically to their project. In my mind, there are only two things that could make Bootstrap better:

  1. Sass!
  2. The ability to make custom semantic CSS classes.

Pamela Fox and BVision explained the second issue very well. The gist: Bootstrap gets updated, or you want to switch frameworks, and now your HTML, JavaScript, CSS, and possibly even backend code need to be updated to reflect the new CSS class names. What a nightmare! What’s more, some of Bootstrap’s class names are non-semantic, giving developers little clue of what their code will change.

I’m going to explain to you how to use CSS class names that make sense to you while benefiting from the awesome power that is Twitter Bootstrap. Keep in mind that I’m using the 3.0.0-wip branch. I’m not sure if this specific fix works for 2.x, but the detective work described in this post is likely the same (and will likely need to be reapplied as the branch is developed.. such is living on the bleeding edge!). If you’re pressed for time, here’s the summary:

  1. Download Bootstrap off their GitHub 3.0.0-wip branch and build it with make. It’s explained how to do this in the README.
  2. Remove a couple LESS comment blocks in bootstrap.css that will cause errors. You can find them by searching for LESS variables that start with @. Or just use your bash debug console. :)
  3. Rename boostrap.css to boostrap.scss and @import into your blank scss file.
  4. Apply span-fix below.
  5. Edit any JavaScript you’re using to reflect your chosen class names.
  6. @extend away!
// My span-fix

@import "bootstrap.scss";

// Span fix
// Place at the top of your sass

@for $i from 1 through 12 {
  .span#{$i} {
    @extend [class^="span"];
    @extend [class*="span"];
  }
}
// How to use

// A contrived example of a row that
// switches context with an 'active' class
//
// Uses jQuery for a click handler to the 'active' class

.animal-descriptions {
  @extend .row;
  margin-bottom: 20px;

  div {
    @extend .span2;
    background: black;
    height: 100px;
  }

  div.first {
    @extend .span5;
    @extend .offset3;
    background: red;
  }

  &.active {
    div.first {
      @extend .span12;
      margin-left: 0; /* undoes offset3 */
    }

    div {
      @extend .span6;
    }
  }
}

# The Breakdown

SCSS, one of Sass’s two syntaxes, is a superset of CSS, and will allow you to mix regular ol’ CSS with SCSS. By importing Bootstrap’s plain-jane CSS into an SCSS file, you’ll be able to create custom classnames that @extend from Bootstrap’s default ones, like .btn.

But Sass won’t import a plain CSS file into an SCSS file with @import. So you need to rename bootstrap.css to .scss, and @import that at the top of your scss file for Sass to actually parse it.

One thing that tripped me up was using spans like .span6 and the like. Simply @extending a span will not work. It turns out that Bootstrap uses CSS attribute selectors for classnames in spans and spans alone, in a variety of situations. Here are just a few I found:

[class^="span"] { ... }

[class*="span"].pull-right { ... }

table col[class^="span"] { ... }

input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input[class*="span"] { ... }

.input-append input[class*="span"],
.input-append .uneditable-input[class*="span"],
.input-prepend input[class*="span"],
.input-prepend .uneditable-input[class*="span"] { ... }

Luckily all the other attribute selectors I found were for HTML attributes that weren’t class names, so they should apply regardless.

By applying the span-fix above, all [class^="span"] and [class*="span"]  selectors will apply to your @extend .span{#} sass. The good news is that Sass seems to be smart enough not to copy all the properties (and thereby bloat your compiled CSS), and instead will attach your new classnames along with the default selectors in the base. Sass will even add your class to states, classes, and attribute selectors where appropriate. For instance, if you @extend btn;, your class also gets automatically added alongside the .btn:hover and .btn[disabled] selectors as .your-class:hover and [disabled].your-class. Robust and efficient, that Sass compiler.

I’ve applied the process above to reworking the “Jumbotron” (RIP Hero Unit) example in the Getting Started section of Bootstrap’s online documentation, to great success. If you try this, do note that the .btn-navbar and .brand classes in 2.3.1 have changed to .navbar-toggle and .navbar-brand, respectively, in 3.0.0-wip. The only other caveats are any JavaScript that apply to the page you’re working on. You’ll need to dive into those and see if they rely on any of the default classnames you wish to change.

Media Queries

Sass @extend follows a few simple but important rules:

  1. Sass will take an @extend within selectors defined in the topmost scope (i.e. not inside a media query), and will go through that scope and the scope of all media queries, matching your selector against the extended selector in each, but will NOT mix selectors and properties between different directives. Example:

     // The recommended way to extend classes in Sass
    
     .class1 {
       margin: 10px;
     }
    
     .class2 {
       @extend .class1;
     }
    
     @media screen and (min-width: 768px) {
       .class1 {
         margin: 20px;
       }
     }
    
     @media screen and (min-width: 992px) {
       .class1 {
         margin: 30px;
       }
     }
    
     // Produces the following CSS:
    
     .class1, .class2 {
       margin: 10px; }
    
     @media screen and (min-width: 768px) {
       .class1, .class2 {
         margin: 20px; } }
     @media screen and (min-width: 992px) {
       .class1, .class2 {
         margin: 30px; } }
    
  2. @extend can be in a selector defined inside a media query but will only extend selectors within media queries that are exactly the same. This includes two exact media query blocks defined in different places within the file(s). Example:

     @media screen and (min-width: 100px) {
       .class1 {
         width: 10px;
       }
    
       .class2 {
         @extend .class1;
       }
     }
    
     @media screen and (min-width: 100px) {
       .class1 {
         width: 20px;
       }
     }
    
     // Produces this CSS.
     // Notice that the second media query wins out because it was defined later:
    
     @media screen and (min-width: 100px) {
       .class1, .class2 {
         width: 10px; } }
     @media screen and (min-width: 100px) {
       .class1, .class2 {
         width: 20px; } }
    
  3. @extend cannot be inside a media query if the class being extended exists in another other directive, or topmost. Example:

     // Sure way to put errors in your Sass
    
     .class1 {
       margin: 10px;
     }
    
     @media screen and (min-width: 100px) {
       .class1 {
         margin: 10px; /* or 20px, or whatever */
       }
    
       .class2 {
         /* Throws an error! Because class1 is also defined outside the scope of this media directive */
         @extend .class1;
       }
     }
    

It’s important to note that you cannot extend selectors into media query selectors. Only extensions made to a selector that’s already inside that @media directive (it can be in another media block of the same name) are applied. A full description of this can be found in the Sass Documentation. I found that once I used @extend, whether in a @media block or not, the media queries would inherit from themselves automatically.

Therefore, the recommended way to use @extend in your Sass is to:

  1. Always @extend from the topmost scope, i.e. outside of media queries.
  2. Only @extend within @media if you’re absolutely sure the class to be @extended only exists inside a directive of equal definition, including the directive you’re writing in.

Conclusion

We’ve now learned how to safely abstract our CSS classes in such a way that they’re infinitely more flexible. We can now upgrade frameworks, switch frameworks, or roll our own safely, because our CSS classes are not tied to any particular design, other than our own. Hooray!

Comments