Sass

Snugug edited this page Jun 21, 2013 · 15 revisions

Sass is a CSS Preprocessing language allowing developers to easily create advanced and maintainable CSS. It's an extension of CSS, adding nesting, variables, selector inheritance, functions, mixins, and control directives, and more including a lot of useful functions. Instructions for installing Sass across all major operating systems are available.

Sass has two syntaxes, .sass and .scss. The former is the original syntax, is white space delineated, and requires no curly braces or semicolons. The later is a true superset of CSS level 3, allowing for any valid CSS to be written in it and be returned one-for-one (also allowing you to rename .css files .scss files and have them be valid). The standard we will be using is .scss.

Nesting

Nesting is a way to easily write compound selectors. Take for example the following CSS:

.foo {
  content: 'foo';
}

.foo .bar {
  content: 'bar';
}

We can express this through nesting in the following way:

.foo {
  content: 'foo';
	
  .bar {
    content: 'bar';
  }
}

By nesting the selector .bar underneath the selector .foo, we are able to see how .bar behaves when scoped to .foo inside of the context of .foo.

IT IS IMPORTANT TO KEEP IN MIND that your nesting should not reflect your HTML source. Nesting is a tool for writing CSS, and if you nest too deep, your selectors will be too specific in their output. Sass does not produce bad output, bad input does. Use Sass as a tool to write good output; all of your CSS best practices still apply. A general rule to keep in mind is the Inception Rule: never nest more than four levels deep.

Selector Referencing

When nesting, we have a way of indirectly referencing the selector we're nested within. We do this with the & (sometimes referred to as "the wonderful, magical ampersand"). It currently behaves in two different ways, either as a combiner or as a reference point. Take, for example, the following CSS:

a {
  color: red;
}

.foo a {
  color: green;
 }

a:hover,
a:target {
  color: blue;
}

We can express this through nesting in the following way:

a {
  color: red;
  
  .foo & {
    color: green;
  }
   
  &:hover,
  &:target {
    color: blue;
  }
}

By making use of the &, we are both able to see in-line how context can change our selection and how we can attach pseud-elements and targets (as well as any other selector we choose) to create compound selectors. The & is very useful for creating encapsulated modules in CSS.

Media Query Bubbling

Like normal selectors, it's possible to nest media queries and have them "bubble up". This is very useful for seeing state changes in-line. For those use to writing media queries in groups, there is no performance difference between that and in-line queries, and in-line queries are easier to debug. As such, the standard we will be using is to use in-line media queries. Doing so is easy in Sass. Take, for example, the following CSS:

.foo {
  width: 100%;
}

@media (min-width: 30em) {
  .foo {
    width: 50%;
  }
}

@media (min-width: 50em) {
  .foo {
    width: 25%;
  }
}

We can express this through nesting in the following way:

.foo {
  width: 100%;
  
  @media (min-width: 30em) {
    width: 50%;
  }
  
  @media (min-width: 50em) {
    width: 25%;
  }
}

By nesting out media queries in-line, we're able to encapsulate all the changes as they happen, making our changes easier to maintain. There are tools out there for making maintaining media queries easier. The standard we will be using is the Compass Extension Breakpoint.

Variables

Variables allow you to extract commonly used item, such as colors, font stacks, border definitions, etc… into reusable pieces. Only values may be stored in variables, not, for instance, full CSS properties or full selectors. Take, for example, the following CSS:

.foo {
  color: #e60013;
  background: #f2e600;
  border: 1px solid #e60013;
}

.bar {
  color: #f2e600;
  background: #00c2b1;
  border-right: 1px solid #e60013;
}

We can express this through variables in the following way:

$red: #e60013;
$yellow: #f2e600;
$blue: #00c2b1;
$border-std: 1px solid $red;

.foo {
  color: $red;
  background: $yellow;
  border: $border-std;
}

.bar {
  color: $yellow;
  background: $blue;
  $border-right: $border-std;
}

By moving frequently used items into variables, we are able to give them semantic meaning in our stylesheets and provide a single place to update all values, making each value easier to maintain.

Lists

Variables that contain more than one value are called lists. Lists allow us to store and access related information. Take, for example, the following CSS:

.red {
  color: red;
}

.green {
  color: green;
}

.blue {
  color: blue;
}

We can express this using lists with the following Sass:

$primary-colors: red, green, blue;

.red {
  // First color in the variable $primary-colors
  color: nth($primary-colors, 1);
}

.green {
  // Second color in the variable $primary-colors
  color: nth($primary-colors, 2);
}

.blue {
  // Third color in the variable $primary-colors
  color: nth($primary-colors, 3);
}

We use the Sass native nth() function to allow us to pick and choose which value of the list we would like to use. This allows us to group, for instance, the primary colors together in a single variable and reference them programatically. Lists are especially useful with the @each control directives. There is an additional native Sass function, length(), that will tell you how many items are in a list. If you pass a variable with only one item into that function, it will return a length of 1. In our example above, it will return a length of 3.

Interpolation

Interpolation allows you to print out the value of a variable in places where it otherwise wouldn't. While variables used as values in properties print out as expected, if you would like to include a variable as part of a printed string or a CSS selector, you need to interpolate it. Take, for example, the previous CSS. We can express it using interpolation with the following Sass:

$primary-colors: red, green, blue;

.#{nth($primary-colors, 1)} {
  // First color in the variable $primary-colors
  color: nth($primary-colors, 1);
}

.#{nth($primary-colors, 2)} {
  // Second color in the variable $primary-colors
  color: nth($primary-colors, 2);
}

.#{nth($primary-colors, 3)} {
  // Third color in the variable $primary-colors
  color: nth($primary-colors, 3);
}

We are able to print out the value of each value as a class by wrapping their nth() calls in interpolation braces (#{}). This allows us to create dynamic selectors using variables, making writing selectors that follow a pattern easier to maintain. Interpolation is especially useful with control directives

Selector Inheritance

In HTML, we can have an element inherit styling by applying a class to that element. If we would like to have it inherit multiple different types of styling, we can apply multiple classes to it. In Sass, we have the ability to do this entirely from within our CSS, making these inheritances easier to maintain and ensure their cascade. Take, for example, the following HTML and CSS:

<div class="foo block"></div>
<div class="bar block"></div>
<div class="baz block"></div>
.block {
  display: block;
  height: 1em;
  width: 1em;
}

.foo {
  border-radius: .25em;
}

We can express this with selector inheritance with the following Sass:

%block {
  display: block;
  height: 1em;
  width: 1em;
}

.foo {
  @extend %block;
  border-radius: .25em;
}

.bar {
  @extend %block;
}

.baz {
  @extend %block;
}

Which produces the following CSS:

.foo,
.bar,
.baz {
  display: block;
  height: 1em;
  width: 1em;
}

.foo {
  border-radius: .25em;
}

Which we would use with the following HTML:

<div class="foo"></div>
<div class="bar"></div>
<div class="baz"></div>

By using selector inheritance, we are able to move inheritance logic out of our HTML and in to our Sass, making it more maintainable and easier from our styling alone to determine the cascade of our inherited styles.

Functions

Functions allow you to write complex pieces of code in Sass that returns a single value (as could be stored in a variable). Take, for example, the following CSS:

.foo {
  width: 58.3333333% /* 7 / 12 * 100% */
}

.bar {
  width: 26.3157895% /* 5 / 19 * 100% */

We can express this as a function with the following Sass:

@function column-width($target, $context: 12) {
  @return ($target / $context) * 100%;
}

.foo {
  width: column-width(7);
}

.bar {
  width: column-width(7, 19);
}

By using a function instead of writing a solid value, we are able to extract computational logic into reusable functions, making maintaining these pieces of functionality easy. We can have input variables have defaults (as in the first case) to make our most common application of functions easy to deal with. Functions also wrap what are otherwise potentially incoherent outputs in semantic meaning, again making maintenance easier.

Mixins

Mixins are similar to function, except you may write CSS in them, and any written CSS will be written in-line where the mixin is called. A general best practice is to only use Mixins when CSS follows a pattern but output changes on user input. If the output doesn't change based on user input, that set of CSS should be maintained through selector inheritance. Take, for example, the following CSS:

.foo {
  background: #f3ee92; /* Background color */
  color: black; /* It's a bright background, use black for the color */
  border: 3px solid #797749; /* Mix the background color with 50% black for the border color */
}

.bar {
  background: #00c2b1; /* Initial color */
  color: white; /* It's a dark background, use white for the color */
  border: 3px solid #006158; /* Mix the background color with 50% black for the border color */
}

We can express this as a mixin with the following Sass (we make use of the control directives @if and @else here):

$yellow: #f3ee92;
$blue: #00c2b1;

@mixin color-setup($color) {
  background: $color;
  @if (lightness($color) > 50%) {
    color: black;
  }
  @else {
    color: white;
  }
  border: 3px solid mix(black, $color, 50%);
}

.foo {
  @include color-setup($yellow);
}

.bar {
  @include color-setup($blue);
}

By moving our logic for setting up the colors of our block into a mixin, we're able to make decisions about which color to use on-the-fly for each color we choose to use with the mixin, and we move the manual labor of choosing the colors into a reusable piece. Finding patterns and turning them into mixins is one of the best way to reduce complexity and increase maintainability of your CSS documents.

Control Directives

Control directives are a way to build logic into your Sass files, especially your functions and mixins. They provide enhanced flexibility when sussing out patterns to turn in to mixins, functions, or just for writing CSS selectors more efficiently. They are the key to making maintainable and flexible stylesheets with Sass. There are four control directives, @if, @else if, @else, @for, @each, and @while. Interpolation also plays a big part in control directives.

@if, @else if, @else

@if and @else are a pair of directives used for action around logical decisions. You use @if to determine whether a statement is true or false. The code wrapped with @if will be included when the statement is true. If paired with @else, when an @if statement is false, the code wrapped with @else will be included. Take, for example, the following Sass:

@mixin better-fonts($font-family) {
  @if $font-family == 'sans-serif' {
    font-family: 'Helvetica' sans-serif;
  }
  @else if $font-family == 'serif' {
    font-family: 'Garamond' serif;
  }
  @else {
    font-family: 'Courier New' monospace;
  }
}

.sans-serif {
  @include better-fonts('sans-serif');
}

.serif {
  @include better-fonts('serif');
}

.mono {
  @include better-fonts('mono');
}

The CSS we would get out of this would be as follows:

.sans-serif {
  font-family: 'Helvetica' sans-serif;
}

.serif {
  font-family: 'Garamond' serif;
}

.mono {
  font-family: 'Courier New' monospace;
}

In the above example, the first selector, we pass 'sans-serif' into our mixin, which passes our first statement check for our @if, so the Helvetica font stack prints out. In the second selector, we pass 'serif' into our mixin, which fails our first statement check for our @if but passes the @else if, so the Garamond font stack prints out. In the third selector, we pass 'mono' into our mixin, which fails both our @if and our @else if, which triggers the final @else, this printing out our Courier New font stack.

@for

@for is a looping directive for iterating a set number of times. The directive has three parts, an iterator (variable), a start (integer), and an end (integer). @for loops only count up, and only count in integers. The iterator will start at the starting integer and continue through the end integer. Take, for example, the following Sass:

$colors: red orange yellow green blue;
$length: length($colors);

@for $i from 1 through $length {
  li:nth-of-type(#{$length}n + #{$i}) {
    background: nth($colors, $i);
  }
}

The CSS we would get out of this would be as follows:

li:nth-of-type(5n + 1) {
  background: red;
}

li:nth-of-type(5n + 2) {
  background: orange;
}

li:nth-of-type(5n + 3) {
  background: yellow;
}

li:nth-of-type(5n + 4) {
  background: green;
}

li:nth-of-type(5n + 5) {
  background: blue;
}

In the above example, we create a list of colors. We then programmatically find the total number of items in our list and using $i as an iterator, we create a @for loop to loop a total number of times equal to that length. We interpolate the length and the iterator to dynamically create our counter and offset, respectively, for our nth-of-type. Finally, we set the background of each newly created selector to the nth() item of our $colors variable, again using the iterator to properly pick out the correct color. By using loops, we are able to reliably work with patterns, making the output of those patters easier to both write and maintain.

@each

@each is another looping directive, except whereas @for had a set number of iterations, @each allows to iterate for a variable number of times based on number of items in a list. The directive has two parts, an iterator (variable), and a list. The iterator takes on each successive value of the list until it has gone through all items. Take, for example, the following Sass:

$animals: 'dog' 'cat' 'monkey' 'horse';

@each $animal in $animals {
  .#{$animal} {
    background-image: url('/images/#{$animal}.png');   
  }
}

The CSS we would get out of this would be as follows:

.dog {
  background-image: url("/images/dog.png");
}

.cat {
  background-image: url("/images/cat.png");
}

.monkey {
  background-image: url("/images/monkey.png");
}

.horse {
  background-image: url("/images/horse.png");
}

In the above example, we create a list of animals. We then loop over each animal, create a class with the same name as the animal, and create a url for background-image based on the animal. If we were to add 'panda' to the list, we would get another class named .panda {} pulling in the correct image. Like with the @for loop, we are able to reliably work with patters (especially list based patterns), making the output of those patterns easier to both write and maintain.

@while

@while is the final looping directive that Sass provides. It is a slightly different kind of looping directive in that, instead of being based on a presupposed end condition, it is up to you to create the end condition. This makes the both more complex to use but also, potentially, more powerful. Take, for example, the following Sass:

$color: #add0bc;
$j: 1;

@while lightness($color) > 25% {
  .teal-#{$j} {
    background: $color;
  }
  $color: mix(black, $color, 25%);
  $j: $j + 1;
}

The CSS we would get out of this would be as follows:

.teal-1 {
  background: #add0bc;
}

.teal-2 {
  background: #819c8d;
}

.teal-3 {
  background: #607569;
}

.teal-4 {
  background: #48574e;
}

In the above example, we set a color and create a variable we're going to use as an iterator. We set up our @while to check the lightness of our color, and as long as that lightness is greater than 25%, it will create a new class with the color, mixin the color with black, and then increase our iterator. This will continue to happen until our lightness drops below 25%. By allowing us to create custom conditions for looping through patterns, we are able to direct our pattern output in unique and interesting ways, allowing custom control for better maintainability.