Learning Sass
We've already briefly mentioned the benefits of Sass, but it will require a little effort to learn how to use it. That said, it's important to recognize a very important fact about Sass, which makes it easier to learn: every CSS file is a valid SCSS file. Sass itself understands two formats; but the format we'll be using is similar to the CSS syntax and uses the .scss
extension. This means that you can write pure CSS and Sass will recognize it without a problem. This allows you to learn features at your own rate without losing productivity. In cases where you don't know a Sass feature, you can write regular CSS. Later, when you've learned more, you can rewrite the CSS using Sass.
Note
The other format that Sass understands uses indentation rather than brackets in order to define the structure of the file. These files use the .sass
extension.
Before we delve deeper into the benefits of Sass, it is important to remember that browsers don't support the SCSS syntax. This means that we have to use a tool to transform the SCSS file into a CSS file that the browser understands. You're already familiar with converting ES2015 code into ES5, and this process is very similar.
Sass is highly based on the DRY concept—that is, don't repeat yourself. CSS, on the other hand, is often full of repetition, so the Sass code can be much more modular and compact than the equivalent CSS. While writing Sass code, one should look for opportunities to reduce repetition and encourage code reuse.
Sass contains imperative control structures such as control flow and loops as well as constructs that look and act like functions. This means that Sass code can sometimes read a bit more like a program than a style sheet. Thus, it is easy to abstract lots of complicated functions in reusable modules; but it also means that one has to be familiar with how Sass works. That said, if there's ever any confusion, consulting the resulting CSS file usually helps clear things up.
We can't cover every feature that Sass provides. Instead, we'll cover the features that we'll tend to use in our code for this book. If you want, you can always learn more by browsing the Sass documentation (http://sass-lang.com/documentation/file.SASS_REFERENCE.html).
Comments
CSS allows comments in the form of /* … */
. These types of comments are useful for licenses and other text that should be downloaded by a client. But typically, developers will strip out any other comment in production-ready CSS files in order to reduce the size of the file. Sass addresses this issue by providing a single-line comment feature. This single-line comment will never be rendered in the output CSS file, so you can use it as much as you want without worrying about the size of the output file. Single-line comments start with a double forward slash (//
). Sass will continue rendering /* … */
comments, so this is also a useful way of controlling which comments remain in the final output.
Sass can also be configured to eliminate all the comments. If you use this feature, you can still force certain comments to be included in the final output by using /! … */
. This might be useful if you need to include a license in the final output.
Calculation
CSS has long been riddled with magic numbers—values that are rather opaque as to their meaning or how the developer arrived at their value. Consider the following example:
.some-element { width: 560px; }
Other than knowing that something is 560px
wide, this snippet doesn't tell you how the width was determined. This means that it's difficult to come back and later modify the value appropriately. It could be the width of an image or it could be the result of a series of mathematical operations.
With CSS3, we can perform runtime calculations, which are extremely useful. So, we could write the following:
.some-element { width: calc((960px / 12) * 7); }
This begins to provide some context—we have a 960px
grid with 12
columns and the element should be 7
columns wide. So, when we come back later to modify it, we will know what we were initially thinking.
Although most modern mobile browsers support this, it's unwise to use calc
in this way, because it incurs a performance hit since it must be continually evaluated. Because this number never changes, it would be faster if the result of the calculation could be determined only once and inserted into the output CSS.
Sass allows this by allowing us to specify calculations inline with our styles. Consider the following example:
.some-element { width: (960px / 12) * 7; }
When Sass generates the CSS file, it will write 560px
, which means that the browser doesn't have to worry about recalculating this value. But when we look at our own code, it's much easier to read and understand why 560px
is the final result.
Note
The snippets in this section are located at snippets/03/ex1-calculation/
in the code package of this book. When using the interactive snippet playground, select 3: Sassy CSS and Example 1: Calculation.
The expected mathematical operators are available, including addition (+
), subtraction (-
), multiplication (*
), and pision (/
). Modulo (%
) is also supported. If any operand is missing its unit, the unit is inferred based on the previous operands. This means that 5px + 3
becomes 8px
and 10px * 3
is calculated as 30px
.
It should be noted that Sass does not perform mixed unit calculations. One cannot add em
units to px
units, for example. This would cause an error. If you need to do these kind of calculations, CSS3's calc
is your only option.
You can, however, convert a ratio into a percentage. For example:
.some-element { width: ((960px / 12) * 7) / 960px * 100%; }
In the preceding example, we're trying to move from a 960px
grid to a percentage-based layout while maintaining the same ratio. The result of the previous calculation will be 58.3333%
. The multiplication by 100%
converts our calculation into a percentage.
The pision operator is somewhat special—the symbol is also used frequently in CSS. What should the result of the following be?
.some-element { width: 500px / 20; }
In the prior example, Sass would output 500px / 20
, not 25px
. This is because CSS shorthand also uses the forward slash to separate some values. For example, font
accepts the size and the line height (for example, 20px/50px
specifies a 20px
font size and 50px
line-height). Because every the CSS file is also a valid Sass file and the expectation is that the resulting output would match the original CSS files, Sass can't perform any pision in this case. To force pision in this case, we can wrap the expression in parentheses, as follows:
.some-element { width: (500px / 20); }
We can also use various functions to operate on numbers. round(1000px / 3)
would be translated to 333px
, not 333.3333px
. For a list of all the available functions, see http://sass-lang.com/documentation/Sass/Script/Functions.html.
Calculations can also be performed on colors; but it is easier to use color functions (these are listed in the previous link) than it is to use calculations to achieve the desired result.
If you need to perform string concatenation, you can use the addition operator.
For more information regarding Sass calculations, please see http://sass-lang.com/documentation/file.SASS_REFERENCE.html#ope.
Variables
One of the most obvious problems with vanilla CSS is the lack of variables. CSS instead often specifies the same colors, sizes, and images over and over. While this can be mitigated using the cascading nature of CSS, not everyone does so. Let's take a look at a couple of examples. Each example will duplicate the following simple layout:
The HTML we're using for each example is as follows (it won't change for any of our following snippets):
<p class="ui-view-container"> <p class="ui-navigation-bar"> Hello, world! </p> <p class="ui-scroll-container"> <p>...</p> ... </p> <p class="ui-tool-bar"> <a href="#">Add</a> | <a href="#">Remove</a> </p> </p>
Note
The snippets in this section are located at snippets/03/ex2-variables/a…c
in the code package of this book. When using the interactive snippet playground, select 3: Sassy CSS and examples from 2a to 2c.
Although this layout looks pretty simple, it is easy to see how the resulting CSS can quickly grow out of control. See the following example:
// Snippet Example 2a .ui-view-container { position: absolute; padding: 10px; margin: 10px; border: 1px solid black; background-color: #EEE; top: 0; left: 0; bottom: 0; right: 0; box-sizing: border-box; font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif; } .ui-navigation-bar { position: absolute; height: 50px; line-height: 50px; top: 0; left: 0; right: 0; border-bottom: 1px solid black; background-color: rgba(255,255,255,.95); color: #333; box-sizing: border-box; text-align: center; font-size: 20px; font-weight: bold; z-index:1; } .ui-scroll-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 50px 10px; overflow: auto; } .ui-tool-bar { position: absolute; bottom: 0; left: 0; right: 0; padding: 0 10px; line-height: 50px; height: 50px; box-sizing: border-box; background-color: rgba(255,255,255,.95); color: #333; border-top: 1px solid black; z-index:1; } .ui-tool-bar a { color: #369; text-decoration: none; display: inline-block; line-height: 50px; }
Now, this isn't good CSS—it's very repetitive and it isn't taking advantage of CSS's cascading nature. So, let's rewrite it to be a bit less repetitive and easier to maintain:
//Snippet Example 2b .ui-view-container, .ui-navigation-bar, .ui-tool-bar, .ui-scroll-container { box-sizing: border-box; position: absolute; } .ui-view-container, .ui-scroll-container { top: 0; left: 0; bottom: 0; right: 0; } .ui-navigation-bar, .ui-tool-bar { background-color: rgba(255,255,255,.95); color: #333; height: 50px; left: 0; right: 0; padding: 0 10px; z-index: 1; } .ui-navigation-bar, .ui-tool-bar, .ui-tool-bar a { line-height: 50px; } .ui-view-container { border: 1px solid black; background-color: #EEE; font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif; margin: 10px; padding: 10px; } .ui-navigation-bar { border-bottom: 1px solid black; font-size: 20px; font-weight: bold; text-align: center; top: 0; } .ui-scroll-container { overflow: auto; padding: 50px 10px; } .ui-tool-bar { border-top: 1px solid black; bottom: 0; } .ui-tool-bar a { color: #369; display: inline-block; text-decoration: none; }
The prior code has improved, but it still isn't clear and it has a lot of magic numbers whose meanings aren't immediately clear (this becomes worse in larger style sheets). With variables, we can start eliminating these magic numbers and improve our code's readability. We can also make it easy to adjust our app's appearance in the future by changing variables in a single location rather than having to search all over our style sheet looking for specific occurrences and making replacements.
Sass defines variables in the following manner:
$variable: value;
With this knowledge, we can now write a theme-able version of our CSS using just a few variables:
//Snippet Example 2c $text-color: #333; $font: "Helvetica Neue", Helvetica, Arial, sans-serif; $text-font: 16px $font; $text-title-size: 20px; $text-title-weight: bold; $tint-color: #369; $bar-background-color: rgba(255,255,255,.95); $view-background-color: #EEE; $separation-border: 1px solid black; $separation-size: 10px; $bar-height: 50px; .ui-view-container, .ui-navigation-bar, .ui-tool-bar, .ui-scroll-container { box-sizing: border-box; position: absolute; } .ui-view-container, .ui-scroll-container { top: 0; left: 0; bottom: 0; right: 0; } .ui-navigation-bar, .ui-tool-bar { background-color: $bar-background-color; color: $text-color; height: $bar-height; left: 0; right: 0; padding: 0 $separation-size; z-index: 1; } .ui-navigation-bar, .ui-tool-bar, .ui-tool-bar a { line-height: $bar-height; } .ui-view-container { border: $separation-border; background-color: $view-background-color; font: $text-font; margin: $separation-size; padding: $separation-size; } .ui-navigation-bar { border-bottom: $separation-border; font-size: $text-title-size; font-weight: $text-title-weight; text-align: center; top: 0; } .ui-scroll-container { overflow: auto; padding: $bar-height $separation-size; } .ui-tool-bar { border-top: $separation-border; bottom: 0; } .ui-tool-bar a { color: $tint-color; display: inline-block; text-decoration: none; }
If you're counting lines, you may notice that this example is actually longer than our previous example. So, at first, this may not appear to offer much benefit. Once you study the code, however, you'll notice several improvements, including the following points:
- The code is more readable and understandable. We don't have to guess what
50px
might mean—the code specifically indicates that it is the height of a bar widget. - The code has fewer magic numbers. Nearly every value (other than zero oro one) is represented by a variable instead of an opaque number. This isn't to say that every value in your Sass should be a variable; but when you find yourself typing the same value over and over in similar contexts, it might be a good time for you to use a variable.
- It's easy to change the resulting appearance. All we have to do is modify the first few lines of the preceding code and change our values. If we want the bars to be
44px
high instead of50px
, it would be an easy change. In the second snippet, it's still easy, but we'd have to make the change in several places. If we did a search and replace, we'd have to make the untenable assertion that each instance of50px
should be changed. In our particular case, this would be safe, but in large files, it's less likely to be correct.
Note
Sass variables are scoped to the selector in which they are declared. So, if a variable was declared in .ui-tool-bar
, it would not be accessible to any other selectors. You can force such a variable to be global by appending !global
. But if you find yourself doing this, you may as well take the variable itself out of the selector.
While using variables, you may also find that it is useful to use interpolation. Consider the following snippet:
.ui-navigation-bar { font: $text-title-weight #{$text-title-size}/#{$bar-height} $font; //font: bold 20px/50px"Helvetica Neue", Helvetica, Arial, sans-serif; }
The preceding interpolation (identified with #{$variable-name}
) is useful to avoid parsing issues. In the previous example, the forward slash could be interpreted in many different ways (including pision). To avoid ambiguity (and possibly the wrong result), we can use interpolation.
We could also use interpolation in other places. Consider the following example:
$button-class: button; a.#{$button-class} { ... }
This translates to the following CSS:
a.button { ... }
In the earlier example, we must use interpolation; using a.$button-class
would result in an error.
Nesting
When you read HTML, it's pretty obvious that a hierarchical and nested structure is present. This is quickly lost in most CSS code. This can make it hard to understand how the CSS is accomplishing the final look. We can use Sass's nesting feature to make our code easier to write and understand.
Note
The snippets in this section are located at snippets/03/ex3-nesting/
in the code package of this book. When using the interactive snippet playground, select 3: Sassy CSS and Example 3.
Let's continue using the layout we discussed in the previous section. There are two anchors in the toolbar. Although the anchors are clearly nested within the p
toolbar, it isn't quite as obvious that this is the case while reading the CSS. We can, however, write our Sass as follows:
.ui-tool-bar { border-top: $separation-border; bottom: 0; a { color: $tint-color; display: inline-block; text-decoration: none; } }
Now, it's quite clear that we are styling anchors that live inside toolbars. The generated CSS creates two rules: .ui-tool-bar
and .ui-tool-bar a
out of this nesting.
While nesting, there is a special placeholder we can use: the ampersand (&
). This is useful when we need to use pseudo-elements such as :hover
or the like. This lets us write a rule like the following:
.ui-tool-bar { ... a { ... &:hover { text-decoration: underline; } } }
Sass also provides another form of nesting. CSS often groups properties together. For example, font-family
, font-size
and font-weight
are all related to the element's. In CSS, we'd have to specify each property separately (or use CSS shorthand). But in Sass, we can do as follows:
.some-element { font: { family: sans-serif; size: 16px; weight: bold; } }
Mixins and functions
As we mentioned before, Sass is all about reducing repetition and increasing abstraction. Sass provides mixin and function directives that let us create reusable components, which we can later insert into our styles.
Note
The snippets in this section are located at snippets/03/ex4-mixins/a…d
in the code package of this book. When using the interactive snippet playground, select 3: Sassy CSS and Example 4a to 4d.
Supporting vendor prefixes makes for a good example. It's not uncommon to see something like the following in many style sheets:
.some-element { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; }
Not only is this a bit painful to look at, it's also incredibly repetitive. With Sass, however, we can write a mixin that does this work for us. First, we will define the mixin:
// Snippet Example 4a @mixin box-sizing($value) { -webkit-box-sizing: $value; -moz-box-sizing: $value; -ms-box-sizing: $value; -o-box-sizing: $value; box-sizing: $value; }
This looks quite a bit like the previous snippet. Remember, a mixin is essentially a macro or a template that you can include elsewhere. To use it, we can create the following rule:
.some-element { @include box-sizing(border-box); }
Of course, with variable interpolation, we could go a step further and make this even more abstract, as in the following snippet:
// Snippet Example 4b @mixin prefix($property, $value...) { -webkit-#{$property}: $value; -moz-#{$property}: $value; -ms-#{$property}: $value; -o-#{$property}: $value; #{$property}: $value; } @mixin box-sizing($value) { @include prefix (box-sizing, $value); }
Note
The …
token in the previous example is used to indicate that the parameter may take multiple parameters. This is similar to the …
operator in ES6.
Finally, we can abstract this one step further:
// Snippet Example 4c $prefixes: "-webkit-","-moz-","-ms-","-o-",""; @mixin prefix($property, $value...) { @each $prefix in $prefixes { #{$prefix}#{$property}: $value; } }
This is an example of the control flow and looping that Sass provides. It is important to remember, however, that there is no runtime performance impact—Sass will unroll the loop when the preceding is converted into CSS.
Note
This prefix
mixin won't handle every case where vendor prefixes are used; but for our example, it is sufficient. One case our mixin doesn't handle is that of vendor prefixes in property values.
If you find yourself reusing the same calculation over and over, the function
directive may come in handy. For example, let's assume that we want to work with a 960px
grid, but we don't want to have a lot of calculations throughout our code. We could write a function as follows:
// Snippet Example 4d $grid-width: 960px; $grid-columns: 12; @function grid-size($columns) { @return ($grid-width / $grid-columns) * $columns; }
We can use this as follows:
.some-element { width: grid-size(5); }
Note
You can pass parameters by name to mixins and functions as well. For example, grid-size($columns: 5)
.
Object-oriented CSS
While mixins provide a great way to reuse code, there's yet another way we can reduce repetition and encourage abstraction. Sass provides us with a mechanism called the @extend
directive.
Let's consider a simple CSS example:
.button { ... } .shiny-button { ... }
If we want to create a shiny button, we will need two classes attached to the element (button shiny-button
). This can be error-prone, since we need to remember to include both classes; it can also create lengthy class lists. Sass's @extend
keyword can simplify this. Consider the following version:
.button { ... } .shiny-button { @extend .button ... }
When we create an HTML element with a class of shiny-button
, it will automatically receive all the rules given to button
without us having to explicitly list it in the class list.
Sometimes, however, we may want to reuse styles without cluttering our CSS. If the base class is never going to be used on its own, there's little reason to generate a class name for it. In this case, Sass has the concept of placeholders. Consider the following example:
%button { ... } .shiny-button { @extend %button ... }
Now, we can create an HTML element with a class of shiny-button
like we did earlier, but we can't create an element with just a class of button
, because this class doesn't exist.
Note
There is a form of object-oriented CSS called OOCSS. If you want to learn more, see http://appendto.com/2014/04/oocss/. I've used object-oriented in the title of this section simply because it is the best way to describe what Sass does while using @extend
.
Modules and partials
CSS lets us import styles from other style sheets and Sass extends this a little further by allowing your Sass files to import other Sass files. The syntax is largely the same as CSS, except that the file extension is omitted. Consider the following example:
@import "utils", "grid";
The previous command will look for an util.scss
and a grid.scss
file in the same directory and import their rules. If you ever need to import a CSS file, you can:
@import "vanilla.css";
An imported Sass file may not be something that you would wish to have translated to CSS on its own. If this is the case, you can prepend an underscore to the filename in the filesystem. For instance, using our preceding example, we could have a _util.scss
file instead of util.scss
. @import
would look and work the same, but Sass will never attempt to create a _util.css
file (without the underscore, it would attempt to create an util.css
file). Which one you use depends largely on whether or not you desire a corresponding CSS file to be generated.