Handy Advanced Sass
I was really quite excited when I first came across Sass. At the time, it was purely Sass variables that pinned my interest. They’ve improved my productivity tremendously but there was so much more to learn and having dug deeper into Sass, there is simply no way I would choose to work without Sass as part of my development stack anymore.
In this article I am going to walk through a few of the examples why and hopefully I’ll be able to share something with you that you weren’t previously aware of.
Control Directives
This is when things start to become more akin to a programming language than CSS and they even come with a warning…
Note that control directives are an advanced feature, and are not recommended in the course of day-to-day styling. They exist mainly for use in mixins, particularly those that are part of libraries like Compass, and so require substantial flexibility.
But don’t let that put you off. Once you have got to grips with these, you can speed up your productivity ten-fold.
@if
The @if
directive takes a condition to evaluate and returns the nested styles if the condition is truthy (not false
or null
). Specifying what to return if the condition is falsey can be done using the @else
statement.
For example, you could create a mixin that handles text insetting based on how light or dark the text colour is.
@mixin text-inset($colour, $opacity: 0.7) {
@if lightness($colour) < 50% {
// if the text colour is a dark colour
// we need a white shadow, bottom, right
text-shadow: 1px 1px 1px rgba(#fff, $opacity);
} @else {
// else it must be light so we need
// a black shadow, top, left
text-shadow: -1px -1px 1px rgba(#000, $opacity);
}
// set the text colour
color: $colour;
}
The lightness function used above is one of many API Functions that Sass provides and we will cover more on these later. This particular function returns a number between 0% and 100% that represents the hue component of a colour. Therefore, we can check if the text is a light or dark colour and apply the text inset accordingly.
Check out this Pen!@for
The @for
directive iterates over it’s contents a set number of times passing a variable whose value increases for each iteration. For instance, if you find yourself repeating similar CSS you can save yourself a lot of manual work by using a for loop.
$columns: 4;
@for $i from 1 through $columns {
.cols-#{$i} {
width: ((100 / $columns) * $i) * 1%;
}
}
Which will compile to the following:
.cols-1 {
width: 25%;
}
.cols-2 {
width: 50%;
}
.cols-3 {
width: 75%;
}
.cols-4 {
width: 100%;
}
In this example, the $i
variable becomes the iteration number from 1 to (and including) 4 so we can use it to create our different selectors and calculate our column percentage widths with some simple math.
You can also iterate from 1 to (and not including) 4 by changing the word through
to to
in our for statement.
$columns: 4;
@for $i from 1 to $columns {
.cols-#{$i} {
width: ((100 / $columns) * $i) * 1%;
}
}
This will only iterate 3 times instead of 4 so would not generate our final .cols-4
declaration in the example above.
@each
The @each
statement is similar to @for
in that it is a loop but there is no incremental iteration variable, instead you define a variable to contain the value of each item in a list.
$icons: success error warning;
@each $icon in $icons {
.icon-#{$icon} {
background-image: url(/images/icons/#{$icon}.png);
}
}
Which would generate:
.icon-success {
background-image: url(/images/icons/success.png);
}
.icon-error {
background-image: url(/images/icons/error.png);
}
.icon-warning {
background-image: url(/images/icons/warning.png);
}
@while
The @while
statement will continually iterate and output it’s nested styles until it’s condition evaluates to false
.
$column: 4;
@while $column > 0 {
.cols-#{$column} {
width: 10px * $column;
}
$column: $column - 2;
}
This will only loop twice as we are subtracting 2 from the $column
variable in each iteration so when it attempts to loop a third time, $column
will not be greater than 0 and the output would be:
.cols-4 {
width: 40px;
}
.cols-2 {
width: 20px;
}
I have used while loops in the past when I wanted to reverse the output of @for
or @each
loops due to CSS specificity.
Passing Blocks of CSS to Mixins
Not only can mixins encapsulate the contents of a selector, they can be passed blocks of CSS which will be compiled wherever you specify the @content
directive. This can be extremely useful for providing context or when working with CSS directives e.g. @media
queries and vendor-prefixed @keyframes
.
A Basic Example
@mixin apply-to-ie7 {
// IE7 hack from
// https://www.paulirish.com/2009/browser-specific-css-hacks/
*:first-child+html & {
@content;
}
}
Usage
.btn {
display: inline-block;
@include apply-to-ie7 {
overflow: visible;
display: inline;
zoom: 1;
}
}
Anything you write inside the curly braces of the @include
will be generated in place of the @content
directive in our mixin.
CSS Output
.btn {
display: inline-block;
}
*:first-child+html .btn {
overflow: visible;
display: inline;
zoom: 1;
}
Advanced Media Query Example
The content directive can also be called repeatedly, which may seem strange but it can actually be really helpful. For instance, I have used it in the past to make my media queries more manageable.
// breakpoints defined in settings
$break-medium: 31em !default;
$break-large: 60em !default;
$break-x-large: 75em !default;
@mixin breakpoint($type, $fallback:false, $parent:true) {
@if $type == medium {
@media (min-width: $break-medium) {
@content;
}
}
@if $type == large {
@media (min-width: $break-large) {
@content;
}
}
@if $type == x-large {
@media (min-width: $break-x-large) {
@content;
}
}
@if $fallback {
@if $parent {
.#{$fallback} & {
@content;
}
} @else {
.#{$fallback} {
@content;
}
}
}
}
Usage
.features__item {
width: 100%;
}
.foo {
color: red;
}
.bar {
color: green;
// use inside a declaration
@include breakpoint(medium) {
color: red;
}
@include breakpoint(large, lt-ie9) {
color: blue;
}
}
// use outside any declarations
@include breakpoint(medium) {
.features__item {
width: 50%;
}
.foo {
color: blue;
}
}
// remember to tell the fallback that
// it's not within a declaration
@include breakpoint(large, lt-ie9, false) {
.features__item {
width: 25%;
}
.foo {
color: green;
}
}
CSS Output
.features__item {
width: 100%;
}
.foo {
color: red;
}
.bar {
color: green;
}
@media (min-width: 31em) {
.bar {
color: red;
}
}
@media (min-width: 60em) {
.bar {
color: blue;
}
}
.lt-ie9 .bar {
color: blue;
}
@media (min-width: 31em) {
.features__item {
width: 50%;
}
.foo {
color: blue;
}
}
@media (min-width: 60em) {
.features__item {
width: 25%;
}
.foo {
color: green;
}
}
.lt-ie9 .features__item {
width: 25%;
}
.lt-ie9 .foo {
color: green;
}
Calling @content
more than once allows me to generate the necessary media query based on $type
and duplicate styles for an IE fallback.
The Extend Directive
You may have noticed that your stylesheets can become rather large when using Sass’s @include
directive. This is because every time you @include
, you are duplicating the styles instead of grouping your declarations. Thankfully, Sass provides the option to @extend
a class which will group your declarations and avoid unnecessary bloat.
A Basic Example
.inline-block {
display: inline-block;
vertical-align: baseline;
*display: inline;
*vertical-align: auto;
*zoom: 1;
}
.btn {
@extend .inline-block;
padding: 0.5em 1em;
}
.foo {
@extend .inline-block;
color: red;
}
CSS Output
// selectors are grouped
.inline-block, .btn, .foo {
display: inline-block;
vertical-align: baseline;
*display: inline;
*vertical-align: auto;
*zoom: 1;
}
.btn {
padding: 0.5em 1em;
}
.foo {
color: red;
}
Instead of adding the styles from .inline-block
into both of our .btn
and .foo
declarations, the extend directive grouped our classes alongside the .inline-block
class, reducing duplication significantly.
Placeholder Selectors
In the previous example our .inline-block
class was also generated amongst the group of declarations and had we not even extended the .inline-block
class, it still would have been output in our final CSS file. This is not ideal and is exactly where Placeholder Selectors come in handy.
Any CSS declared within a placeholder will not be compiled in your final CSS file unless it has been extended. It also means that other developers cannot inadvertently hook onto these styles in their markup. I therefore prefer to extend placeholders instead of classes at all times and reserve classes for their intended role as markup hooks.
Placeholders are declared and extended exactly the same way as classes except the dot is replaced with a percentage symbol:
%inline-block {
display: inline-block;
vertical-align: baseline;
*display: inline;
*vertical-align: auto;
*zoom: 1;
}
.btn {
@extend %inline-block;
padding: 0.5em 1em;
}
.foo {
@extend %inline-block;
color: red;
}
CSS Output
.btn, .foo {
display: inline-block;
vertical-align: baseline;
*display: inline;
*vertical-align: auto;
*zoom: 1;
}
.btn {
padding: 0.5em 1em;
}
.foo {
color: red;
}
Now, the selectors are grouped as before but without an .inline-block
class in the group. Much better!
When to Placeholder
A general rule of thumb that I tend to stick to is that if there is a block of CSS you would have bundled into a mixin but the mixin does not require any paramaters (such as a clearfix mixin) then it would probably be better off as a placeholder.
Having said that, due to the media directive’s inability to extend an external placeholder, I sometimes keep the mixin for use within media queries and then include it in my placeholder for use everywhere else:
// keep the mixin for use
// within media queries
@mixin clearfix {
*zoom: 1;
&:before,
&:after {
content: "";
display: table;
}
&:after {
clear: both;
}
}
// use the placeholder
// for everything else
%clearfix {
@include clearfix;
}
Nesting Placeholders
Placeholders are much like any other selector so they can be nested:
%grid {
overflow: hidden;
%column {
min-height: 1px;
}
@for $i from 1 through 12 {
%columns-#{$i} {
width: ((100 / 12) * $i) * 1%;
}
}
}
.foo,
.bar {
@extend %grid;
}
.baz {
@extend %column;
@extend %columns-5;
}
.qux {
@extend %column;
@extend %columns-3;
}
… which would generate the following:
.foo,
.bar {
overflow: hidden;
}
.foo .baz,
.bar .baz,
.foo .qux,
.bar .qux {
min-height: 1px;
}
.foo .qux,
.bar .qux {
width: 25%;
}
.foo .baz,
.bar .baz {
width: 41.66667%;
}
If we focus on the second block of CSS within the output above, this happens because anything that extends %column
will be nested within anything that extends %grid
unless we also nest our @extend
. So, if you wanted to stop .baz
from nesting within .bar
, you would do this:
.foo {
@extend %grid;
.baz {
@extend %column;
@extend %columns-5;
}
}
.bar {
@extend %grid;
}
.qux {
@extend %column;
@extend %columns-3;
}
… which generates:
.foo,
.bar {
overflow: hidden;
}
.foo .baz,
.foo .qux,
.bar .qux {
min-height: 1px;
}
.foo .qux,
.bar .qux {
width: 25%;
}
.foo .baz {
width: 41.66667%;
}
Furthermore, due to the placeholder nesting, if you were to extend %column
but not extend %grid
, there would be no CSS generated because %column
can only exist within %grid
.
Advanced Example
Obviously the examples above are completely useless but it demonstrates the possibilities in a simple way. A more useful/complex example of extending (with placeholders) might be if you wanted to create a grid with two levels of nesting:
%grid {
margin-left: -2em;
overflow: hidden;
%column {
float: left;
padding-left: 2em;
min-height: 1px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@for $i from 1 through 12 {
// top level column spanning
%columns-#{$i} {
width: ((100 / 12) * $i) * 1%;
@for $j from 1 through 12 {
// second level (nested) column spanning
%columns-#{$j} {
@if $j != $i {
$width: ((100 / $i) * $j) * 1%;
@if $width <= 100% {
width: $width;
}
}
}
}
}
}
}
.my-grid {
@extend %grid;
[class^="cols-"],
[class*=" cols-"] {
@extend %column;
}
@for $i from 1 through 12 {
.cols-#{$i} {
@extend %columns-#{$i};
}
}
}
The output is too big to paste here so I’ve put it on pastie but you can see how I only needed to extend %columns-#{$i}
once, yet it generated the necessary column styles for both top level and nested columns.
In theory, this would all make it very easy to create a framework that consisted entirely of placeholders. Then for each project that uses the framework you could extend the modules you need, meaning your resulting CSS would only ever contain the styles you actually use, with the class names of your choice. Yum.
Extending Into Media Queries
I mentioned earlier that it wasn’t possible to extend placeholders defined outside of a media query from within a media query but it is perfectly possible to extend into a media query. This might be easier to understand in code. The following is an example of extending from within a media query which will not work:
%warning {
background-color: red;
}
@media (min-width: 31em) {
.error {
@extend %warning;
}
}
The extend directive will try to find the %warning
placeholder inside the media query, and since it does not exist in that scope, Sass will error1. However, you can do this:
%column {
width: 100%;
}
@media (min-width: 31em) {
%column {
width: 50%
}
}
@media (min-width: 75em) {
%column {
width: 25%;
}
}
.feature__item {
@extend %column;
}
Notice how I’ve defined the same placeholder within the two media queries and extended them from within a declaration outside of the media queries. This will result in:
.feature__item {
width: 100%;
}
@media (min-width: 31em) {
.feature__item {
width: 50%
}
}
@media (min-width: 75em) {
.feature__item {
width: 25%;
}
}
API Functions
There are many functions provided with Sass that can aid development and you have probably already used a few, such as lighten()
, darken()
, rgba()
, but I recently came across some that I wasn’t already aware of that have been a great help when I started building a framework.
if()
Not to be confused with the @if
directive, this function can be used similar to the ternary operator provided in most languages. In Javascript you might do this:
var foo = bar ? 'red' : 'green';
… and in Sass you can do this:
$foo: if($bar, 'red', 'green');
So instead of doing:
.foo {
@if $bar {
width: 100%;
} @else {
width: 50%;
}
}
You can do:
.foo {
width: if($bar, 100%, 50%);
}
Much tidier 🙂
length()
In Sass, variables can become lists simply by adding more than one value to a variable and separating them with a space or a comma. In fact, Sass variables are just lists with one value but what if you need to know how many items are in a list? You can do just that by calling length()
on the list.
$colours: red green blue;
$colourCount: length($colours); // 3
This is really handy when working with loops and we will see an example of this in the next section.
nth()
The nth()
function enables you to grab a specific value from a list by it’s position in the list (from 1 to list length). It takes two parameters – the list that you wish to search and the position of the item you want to retrieve. Used with a loop, this can help make your CSS a lot DRYer.
Originally you might have done something like:
.rgb {
float: left;
width: 100px;
height: 100px;
}
.rgb:nth-of-type(1) {
background-color: red;
}
.rgb:nth-of-type(2) {
background-color: blue;
}
.rgb:nth-of-type(3) {
background-color: green;
}
But there is a lot of repetition that could be reduced if you use a loop with the help of nth()
:
$colours: red green blue;
.rgb {
float: left;
width: 100px;
height: 100px;
}
@for $i from 1 through length($colours) {
.rgb:nth-of-type(#{$i}) {
background-color: nth($colours, $i);
}
}
Check out this Pen!
We’ve looped from 1 to 3 and set the background colour on each element to whichever colour is at position 1 to 3 in the colours variable. Doing this makes things much easier to maintain, especially if you wanted to add another colour (for example).
index()
The index()
function returns the position (from 1 to list length) of a value within a list and will return false
if it cannot find the value. It also takes two parameters – the list to search and the value whose position you want to return. So, we could rewrite our above example to the following:
$colours: red green blue;
.rgb {
float: left;
width: 100px;
height: 100px;
}
@each $colour in $colours {
.rgb:nth-of-type(#{index($colours, $colour)}) {
background-color: $colour;
}
}
It’s up to you which you prefer but index()
and nth()
can be really handy when used together for working with multiple lists. I will be demonstrating this next.
Using nth()
and index()
to Refactor the Media Query Example
There are such things as multi-dimensional lists in Sass and if we used one, we could improve our media query mixin so that you never have to touch the mixin when adding a new breakpoint.
// breakpoints defined in settings
$breakpoints: medium 31em, large 75em, x-large 75em !default;
// mixin defined elsewhere and shouldn't be touched
@mixin breakpoint($type, $fallback:false, $parent:true) {
@each $breakpoint in $breakpoints {
$alias: nth($breakpoint, 1);
$bp: nth($breakpoint, 2);
@if $alias == $type {
@media (min-width: $bp) {
@content;
}
@if $fallback {
@if $parent {
.#{$fallback} & {
@content;
}
} @else {
.#{$fallback} {
@content;
}
}
}
}
}
}
This is a huge step forward since having to update the mixin every time would make it prone to human error. However, we could refactor further if we split the settings variable in two:
// breakpoints defined in settings
$breakpoints: 31em 60em 75em !default;
$breakpoint-aliases: medium large x-large !default;
// mixin defined elsewhere and shouldn't be touched
@mixin breakpoint($type, $fallback:false, $parent:true) {
$pos: index($breakpoint-aliases, $type);
$bp: nth($breakpoints, $pos);
@media (min-width: $bp) {
@content;
}
@if $fallback {
@if $parent {
.#{$fallback} & {
@content;
}
} @else {
.#{$fallback} {
@content;
}
}
}
}
Here we use index()
to find the position of the $type
within the aliases variable and then use that position to get the breakpoint for that alias. Now we’re left with a much smaller, cleaner mixin that shouldn’t need to be modified.
Conclusion
I have only just touched on some of the more advanced features of Sass but hopefully you can see how powerful they can be.
I would highly recommend having a read through the Sass Functions documentation to see if there is anything there that might aid your productivity further. Already using some? It’d be great to see any examples you use that you feel we could all benefit from.
Happy Sassing 🙂
- Currently Sass will display a warning but there are plans to make this error in future versions. ↩