Monday, December 17, 2012

Using Sass and Compass to Write Less CSS Better, Faster, and Smarter

One of my favorite perks of building web-based tools is not needing to compile anything. Just open a text editor, write some code, and refresh the browser - instant gratification. So, as I've learning new technologies that are designed to make it easier to build web pages, I've been reluctant to use anything that requires building or compiling code. However, as I've written more and more CSS styles, I've found myself thinking:

  1. Wouldn't it be great if I could just reuse this block of styles and swap out the color.

  2. I need this in several sizes...

  3. The vendor prefixes are driving me nuts!



It seemed time to try a CSS meta-language to reduce the amount of work required to handle those scenarios I kept running into.  Sass seemed like a good starting point - I've seen quite a few references to it and like its syntax.  It really only takes a few minutes to figure out the basics.  I figured if it helped write less CSS, it would be worth it even if it required a build step to get to the final style sheet.

The true tipping point came when I realized I'd need to write the following styles to make my CSS-only spinner work properly across all the browsers:


@-moz-keyframes clock-spinleft {
from {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-moz-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}

@-webkit-keyframes clock-spinleft {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}

@-o-keyframes clock-spinleft {
from {
-o-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-o-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}

@-ms-keyframes clock-spinleft {
from {
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-ms-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}

@keyframes clock-spinleft {
from {
transform: rotate(0deg);
}

to {
transform: rotate(-360deg);
}
}

@-moz-keyframes clock-spinright {
from {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}

@-webkit-keyframes clock-spinright {
from {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

@-o-keyframes clock-spinright {
from {
-o-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}

@-ms-keyframes clock-spinright {
from {
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}

to {
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}

@keyframes clock-spinright {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

.spinner.clock {
position: relative;
height: 46px;
width: 46px;
}

.spinner.clock > div {
position: absolute;
border-style: solid;
background-color: transparent;
}

.spinner.clock .circle {
width: 36px;
height: 36px;
border-width: 5px;
border-color: #555;
-webkit-border-radius: 36px;
-moz-border-radius: 36px;
-ms-border-radius: 36px;
-o-border-radius: 36px;
border-radius: 36px;
}

.spinner.clock .wedge {
width: 0;
height: 0;
border-width: 23px;
border-color: #999 transparent transparent transparent;
-webkit-border-radius: 23px;
-moz-border-radius: 23px;
-ms-border-radius: 23px;
-o-border-radius: 23px;
border-radius: 23px;
-webkit-animation: clock-spinright 1250ms linear 0s infinite;
-moz-animation: clock-spinright 1250ms linear 0s infinite;
-ms-animation: clock-spinright 1250ms linear 0s infinite;
-o-animation: clock-spinright 1250ms linear 0s infinite;
animation: clock-spinright 1250ms linear 0s infinite;
}
.spinner.clock .wedge:before, .spinner.clock .wedge:after {
content: ' ';
position: absolute;
margin: 0;
top: -23px;
left: -23px;
width: 0;
height: 0;
border-style: solid;
border-width: 23px;
border-color: transparent #fff #fff #fff;
-webkit-border-radius: 23px;
-moz-border-radius: 23px;
-ms-border-radius: 23px;
-o-border-radius: 23px;
border-radius: 23px;
}
.spinner.clock .wedge:before {
-webkit-transform: rotate(-30deg);
-moz-transform: rotate(-30deg);
-ms-transform: rotate(-30deg);
-o-transform: rotate(-30deg);
transform: rotate(-30deg);
}
.spinner.clock .wedge:after {
-webkit-transform: rotate(20deg);
-moz-transform: rotate(20deg);
-ms-transform: rotate(20deg);
-o-transform: rotate(20deg);
transform: rotate(20deg);
}

.spinner.clock .arc {
width: 36px;
height: 36px;
border-width: 5px;
border-color: transparent transparent #bbb transparent;
-webkit-border-radius: 36px;
-moz-border-radius: 36px;
-ms-border-radius: 36px;
-o-border-radius: 36px;
border-radius: 36px;
-webkit-animation: clock-spinleft 750ms linear 0s infinite;
-moz-animation: clock-spinleft 750ms linear 0s infinite;
-ms-animation: clock-spinleft 750ms linear 0s infinite;
-o-animation: clock-spinleft 750ms linear 0s infinite;
animation: clock-spinleft 750ms linear 0s infinite;
}



Way too many vendor prefixes to deal with. I'd probably forget to change half of these if I wanted to tweak something. With Sass mixins, I should be able to reduce the amount of duplication due to the vendor prefixes. Sass is a Ruby gem so if you already have Ruby installed, get Sass is pretty straight forward:


gem install sass


Once installed, I just needed to create a SCSS file and wrap my existing CSS with some Sass magic:


@mixin animation($name: none, $duration: 0s, $timing: ease, $delay: 0s, $iteration: 1, $direction: normal, $fillmode: none) {
animation: $name $duration $timing $delay $iteration $direction $fillmode;
-moz-animation: $name $duration $timing $delay $iteration $direction $fillmode;
-webkit-animation: $name $duration $timing $delay $iteration $direction $fillmode;
-o-animation: $name $duration $timing $delay $iteration $direction $fillmode;
-ms-animation: $name $duration $timing $delay $iteration $direction $fillmode;
}

@mixin transform($params) {
transform: $params;
-moz-transform: $params;
-webkit-transform: $params;
-o-transform: $params;
-ms-transform: $params;
}

// https://gist.github.com/1607696
@mixin keyframes($name) {
@-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@content;
}
@-ms-keyframes #{$name} {
@content;
}
@keyframes #{$name} {
@content;
}
}

.spinner > div {
position: absolute;
background-color: transparent;
}

.spinner .circle {
border-color: #555;
width: 30px;
height: 30px;
border-radius: 30px;
border-width: 4px;
}

.spinner .wedge {
width: 0;
height: 0;
border-radius: 19px;
border-width: 19px;
border-color: #555 transparent transparent transparent;
opacity: 0.4;
@include animation(spinleft, 750ms, linear, 0s, infinite);
}

.spinner .arc {
width: 30px;
height: 30px;
border-width: 4px;
border-radius: 30px;
border-color: transparent transparent #999 transparent;
@include animation(spinright, 750ms, linear, 0s, infinite);
}

@include keyframes(spinleft) {
from { @include transform(rotate(0deg)); }
to { @include transform(rotate(-360deg)); }
}

@include keyframes(spinright) {
from { @include transform(rotate(0deg)); }
to { @include transform(rotate(360deg)); }
}


Much less code. A whole lot easier to read too. The next question that crossed my mind was that some helpful person out there probably already made a handy library that contains all those CSS mixins for vendor prefixes and other helpful short-cuts. And, to my delight, there is - Compass has mixins for a whole bunch of CSS3 styles so you don't have to make them all. Like Sass, Compass is a Ruby gem. Once installed, you simple create a project using:


compass create <myproject>


That will create several subdirectories with a config.rb file in the directory and some base Sass files. You simply add your Sass files to the indicated directory and start coding. To use one of the Compass libraries, just import them:


@import "compass/css3";


This pulls in all the available CSS3 mixins defined in Compass. Browse the Compass reference to see all the available functions. Once you want to generate your CSS file, you need to use Compass, not Sass to compile everything:


compass compile <myproject>


Now all the CSS files will be saved in the stylesheets directory in your project folder.

With Compass, I can now remove the mixins I defined because they are in the Compass library. With that change, I've created this final version:


@import "compass/css3";
@import "animation";

@mixin spinner-clock($name: null, $size: 35px) {

$class: "";
@if $name { $class: ".#{$name}"; };

// Need to work with whole numbers
// when divided by 2, otherwise the wedge
// will be offset from the center which is distracting
@if $size%2 > 0 { $size: $size+1; };

$border: ceil(10 * $size / 80);
$half: $size/2;

.spinner.clock#{$class} {
position: relative;
height: $size+$border*2;
width: $size+$border*2;
}

.spinner.clock#{$class} > div {
position: absolute;
border-style: solid;
background-color: transparent;
}

.spinner.clock#{$class} .circle {
width: $size;
height: $size;
border: {
width: $border;
color: #555;
}
@include border-radius($size);
}

.spinner.clock#{$class} .wedge {
width: 0;
height: 0;
border: {
width: $half+$border;
color: #999 transparent transparent transparent;
}
@include border-radius($half+$border);
@include animation(clock-spinright 1250ms linear 0s infinite);


// This does create a minor artifact in FF. The idea was
// to adjust the size of the wedge by sliding another semi-circle
// over it to make it look smaller. Comment it out or remove this section
// if you don't like it.
// or, even better, add a parameter to the mixin to not use it.

&:before, &:after {
content: ' ';
position: absolute;
margin: 0;
top: -1*($half+$border);
left: -1*($half+$border);
width: 0;
height: 0;
border: {
style: solid;
width: $half+$border;
color: transparent #fff #fff #fff;
}
@include border-radius($half+$border);
}

&:before {
@include transform(rotate(-30deg));
}

&:after {
@include transform(rotate(20deg));
}

}

.spinner.clock#{$class} .arc {
width: $size;
height: $size;
border: {
width: $border;
color: transparent transparent #bbb transparent;
}
@include border-radius($size);
@include animation(clock-spinleft 750ms linear 0s infinite);
}

}

@include keyframes(clock-spinleft) {
from { @include rotate(0deg); }
to { @include rotate(-360deg); }
}

@include keyframes(clock-spinright) {
from { @include rotate(0deg); }
to { @include rotate(360deg); }
}


That code will generate my spinner with all the different vendor prefixes with the additional perk of being able to specify an optional size. Since I was using Sass, I figured why not add some extra flexibility and reuse to the design of my spinner by making it a Sass mixin.

You might notice the @import "animation" at the beginning of the script. As it turns out, Compass does not have anything to generate the animation style or keyframes definition. However, there is a Compass extension that does add this support. Just install the gem:


gem install animation --pre


Open the Compass config.rb file in your project directory and add this line:


require 'animation'


Now that the required libraries are added, you can use the spinner mixin to generate all the required CSS for a specific size:


@import "spinner-clock";

@include spinner-clock("size15", 15px);
@include spinner-clock("size35", 35px);
@include spinner-clock("size55", 55px);


In this example, I named the spinner mixin code file _spinner-clock.scss and the above code css-spinner-clock-custom.scss. Once compiled, I'll be left with a css-spinner-clock-custom.css file with 3 distinct sets of spinner CSS style definitions each named differently (.size15, .size35, and .size55). In the HTML file, I just add the appropriate markup referencing the required styles:


<div class="spinner clock size15">
<div class="wedge"></div>
<div class="circle"></div>
<div class="arc"></div>
</div>

<div class="spinner clock size35">
<div class="wedge"></div>
<div class="circle"></div>
<div class="arc"></div>
</div>

<div class="spinner clock size55">
<div class="wedge"></div>
<div class="circle"></div>
<div class="arc"></div>
</div>


Here, the extra clock class is used to distinguish from other spinners I might make later. The most current version and full source, including an already generated CSS file, is available on GitHub. I also published a quick demo on the project page for the repository. In addition to the spinner described here, I've also made several other variations all using Sass/Compass as a basis for generating the required CSS.

Now these spinners don't work in all versions of IE (only version 10), but it illustrates the power of using a meta-language to define CSS style sheets. You can quickly appreciate the readability of the code and ability to organize and reuse common styles. I had to do a little extra work to get everything compiled and rolled out. However, the benefits were definitely worth the extra build step. Despite my attempts, it looks like I'll never be able to completely ditch the compiler.