Get Inside Unbounce

Subscribe

Handling Code Duplication With Sass @imports in The Asset Pipeline

I have used the asset pipeline heavily since its release in Rails 3.1.0 over two years ago. Only recently did I run in to a handful of gotchas when it came to dealing with stylesheet compilations using Sass.

The asset pipeline had been used mostly for compilation and minification of coffeescript. Not much time was spent on the stylesheet-side development. Depending on the service we may have used Sass or Less but in either case the only feature we really took advantage of from either was nesting rule definitions.

This made our CSS compilations very simple. We used the pipeline-provided require directives and it compiled the stylesheets without a problem. Occasionally we would see a load-order or dependency issue that was simply resolved by requiring one file before the other.

A simple example:

/*
 *= require 'base'
 *= require 'fonts'
 *= require 'styles'
 */

This would result in a compiled css file as such:

# rules from base.css …
# rules from fonts.css …
# rules from styles.css …

The first time I ran into an issue with this pattern was when we began using more of Sass’s features, including mixins, functions & variables. We wanted to define sets of commonly-used mixins and variables and have access to them in all of our stylesheets. The plan was a very common use-case and one of the reasons Sass comes highly recommended.

We started by defining a common.css.scss file with some variables and functions:

@mixin content-area() {
      background: $light-blue;
      box-shadow: 0 5px 0 -2px rgba(0, 170, 255, 0.10);
      padding: 42px 20px;
}

$burgundy:   #68332f;
$light-blue: #f5fbfc;
$red:        #dc1730;

Then utilized the new mixins in the styles.css.scss file:

.callout-bucket {
    @include content-area;
    height: 252px;
}

Now require both files in application.css:

/*
 *= require 'common'
 *= require 'styles'
 *= require 'other'
 */

And tada! We get an exception.

Error compiling asset styles.css:
Sass::SyntaxError: Undefined mixin 'content-areas'

What is going on?

I thought Sass was supposed to help me with code reuse, what gives?

When the asset pipeline parses files it does so file by file, pre-processor by pre-processor (from right to left). This means the common.css.scss file is where it starts. As the scss extension is the rightmost extension Sass begins by running the file through the Sass pre-processor. It makes the mixins and variables available for use within the current Sass environment scope, parses the file, and ends. The pipeline then moves on to the next file styles.css.scss. Sass starts up a new environment scope again. It attempts to use the mixin defined in common.css.scss but it is not defined. This is because common.css.scss and styles.css.scss are not being processed within the same scope.

Fixing It: The Wrong Way

My first thought to fix the problem was: “oh I need to include those functions into each and every file I want to utilize them in.”

So I adjusted my styles.css.scss and other.css.scss files and included the common.css.scss file at the top of both.

style.css.scss:

@import 'common'

.callout-bucket {
    @include content-areas;
    height: 252px;
}

other.css.scss:

@import 'common'

// code for other.css.scss …

This fixes the exception raised by the pre-processor and compiles my css as I expected. My page styles load and everything seems great. Upon a visual inspection of the compiled css it reveals the file is structured as such:

# rules from base.css …
# rules from styles.css …
# rules from base.css …
# rules from other.css …

This shows us all the common rules were being brought in twice. I also had many other files I imported common in to. This result is thousands of lines worth of repeated rules. This increases the compilation time and overall compiled file size.

This is not a suitable “solution.”

Fixing It: The Right Way

So it turns out that the fix is actually quite simple, and is recommended by both the Rails Guides and the Sass-rails gem readme.

If you want to use multiple Sass files, you should generally use the Sass @import rule instead of these Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the document they were defined in.

The Rails Guide

Sprockets provides some directives … They are very primitive and do not work well with Sass files. Instead, use Sass’s native @import directive which sass-rails has customized to integrate with the conventions of your Rails projects.

sass-rails github

Now that we understand the scope problem we can change our application.css file to be a sass processed file application.css.scss and make all our imports using the Sass @import method:

@import 'bourbon';
@import 'neat';
@import 'common';
@import 'styles';
@import 'other';

This creates a single scope while pre-processing the application.css.scss file and imports all our other files in to that scope. This allows them to have access to any of the variables, functions, and mixins defined before them.

Another common problem: Redundant Includes

Another issue often experienced as an application grows, and with it the size and amount of stylesheets, is that files will often end up importing or mixing in the same rules multiple times.

The sprocket directives, as well as Sass’s @import, do not validate, nor care, if the file being imported has or has not been previously imported.

We’ve experienced this issue here at Unbounce. Here is a single example:

base.css.scss:

@import 'compass/css3/border-radius';       
@import 'compass/css3/images'
@import 'compass/css3/box-shadow';      
@import 'compass/css3/user-interface';

preview.css.scss:

@import 'base';

@import 'compass/css3/border-radius';
@import 'compass/css3/images';
@import 'compass/css3/box-shadow';
@import 'compass/css3/user-interface';

@import 'base';
@import 'base/colours';
@import 'base/mixins/buttons';

@import 'reset.css.scss';
@import 'main.css.scss';
@import 'global.css.scss';
@import 'header.css.scss';

The result is a file with all the css from base.css.scss is included three times over in the final compiled file.

# rules from base (including compass imports) …
# rules from compass import …
# rules from base again (including compass imports) …
# rules from all other imports …

Sass and the Pipeline will both allow you to find yourself in this corner. It is up to the developers to make sure our stylesheets are structured and organized.

Sass is an awesome tool and using it with the asset pipeline can make for a really great development environment. It is however another combination that makes it easy to shoot yourself in the foot when done improperly. Having guidelines for your team to follow will help keep your stylesheets well structured and organized. Knowing the gotchas during setup will save yourself from headache and rule redundencies.