Lesson 19 - Setting up Sass

Course
Learn Eleventy From Scratch
Module
Asset Pipeline

First up in our asset pipeline is our Sass (SCSS) setup.


Before we get into this one: it is a pretty intense lesson, so if you felt like you need a break already, now is the time to have one!

For this project’s CSS, we’re going to be using Sass. If you’ve never heard of it, you’ll find a really good summary on the Sass home page:

Sass is the most mature, stable, and powerful professional grade CSS extension language in the world.

This is exactly how we’ll be using Sass to write our CSS—as an extension of the native language. The main reason why is that it makes working with the CSS methodology we’ll be using—called CUBE CSS—much easier.

Before we dig deep into the front-end, I strongly recommend that you read my article on CUBE CSS.

We will of course cover it well in this course, but understanding why we’re using Sass will be a lot easier when you understand CUBE CSS.

In order to work with Sass, we’re going to create a Gulp task that converts our .scss files into .css files that the browser can parse and understand. This is called preprocessing.

Creating a Sass task permalink

The first thing we need to do is create a folder for all of our Gulp tasks to live in. Open up your terminal in eleventy-from-scratch and run the following:

Code language
bash
mkdir gulp-tasks

Then, create a file in your new gulp-tasks folder called sass.js and add the following to it:

Code language
JavaScript
const {dest, src} = require('gulp');
const cleanCSS = require('gulp-clean-css');
const sassProcessor = require('gulp-sass');

// We want to be using canonical Sass, rather than node-sass
sassProcessor.compiler = require('sass');

We have some dependencies here, so let’s install them. In your terminal, run the following:

Code language
bash
npm install gulp-clean-css gulp-sass@4.1.0 sass

We’re using gulp-sass@4.1.0 because they introduced several breaking changes and I wanted to make sure that if you download the source files, it’ll all work fine for you.

By default, Gulp Sass uses Node Sass, which is fine. But personally, I prefer to use Dart Sass, which is known as canonical Sass. The main reason for this is that I don’t like surprises or compatibility issues, which I’ve run into with Node Sass over the years. Node Sass often finds itself behind canonical Sass in terms of features, too.

Open up eleventy-from-scratch/gulp-tasks/sass.js and add the following after the existing code:

Code language
JavaScript
// Flags whether we compress the output etc
const isProduction = process.env.NODE_ENV === 'production';

// An array of outputs that should be sent over to includes
const criticalStyles = ['critical.scss', 'home.scss', 'page.scss', 'work-item.scss'];

// Takes the arguments passed by `dest` and determines where the output file goes
const calculateOutput = ({history}) => {
  // By default, we want a CSS file in our dist directory, so the
  // HTML can grab it with a <link />
  let response = './dist/css';

  // Get everything after the last slash
  const sourceFileName = /[^/]*$/.exec(history[0])[0];

  // If this is critical CSS though, we want it to go
  // to the _includes directory, so nunjucks can include it
  // directly in a <style>
  if (criticalStyles.includes(sourceFileName)) {
    response = './src/_includes/css';
  }

  return response;
};

You’ve probably noticed that I’ve commented the heck out of this file. This is because Gulp tasks tend to not get touched very often, so comments help to jog our memory when we come back to it in the future.

At the top of this section, we determine if isProduction is true or false. This tells us whether or not we should compress the output.

Then, we create a function called calculateOutput. We pass this into a pipe later in this task to determine where the output file goes. This is because we’re splitting our CSS into critical CSS and standard CSS.

The critical CSS will be inlined in a <style> element in the <head>. The standard CSS is added via a <link> element. This strategy is great for performance and we’ll discuss it in more depth later in the course.

Because we have two defined outputs, the calculateOutput uses criticalStyles to compare the current file against. If it’s critical, we send it to the _includes folder so Eleventy can use it. If not, we send it straight to the dist folder, so the front-end can add it with a <link> element.

Open up eleventy-from-scratch/gulp-tasks/sass.js again and add the following after the existing code:

Code language
JavaScript
// The main Sass method grabs all root Sass files,
// processes them, then sends them to the output calculator
const sass = () => {
  return src('./src/scss/*.scss')
    .pipe(sassProcessor().on('error', sassProcessor.logError))
    .pipe(
      cleanCSS(
        isProduction
          ? {
              level: 2
            }
          : {}
      )
    )
    .pipe(dest(calculateOutput, {sourceMaps: !isProduction}));
};

module.exports = sass;

This is our main Sass Gulp task. Let’s see how it works.

The first thing it does is find all .scss files that it can see in eleventy-from-scratch/src/scss. These are our main Sass files. We then get those files and pass them down the production line into another pipe. This is where the sassProcessor grabs them and runs them through sass.

Once Sass is done, we pass it down the production line to cleanCSS. This will compress, merge and minify CSS based on whether isProduction is true or false. The level: 2 essentially tells it to use a lot of the most aggressive rules.

Lastly, we pass it to our calculateOutput which will determine if it is critical CSS or not.

Letting Eleventy see our critical CSS permalink

By default, Eleventy will ignore everthing that we tell git to ignore with .gitignore. 99% of the time, we wouldn’t even need to worry about this, but because we’re ignoring eleventy-from-scratch/src/_includes/css in our .gitinore file, Eleventy won’t see it. This creates an issue because Eleventy won’t be able to directly include our critical CSS on the page, like this: {% include "css/critical.css" %}.

Luckily, there’s a simple workaround. Open up eleventy-from-scratch/.eleventy.js and add the following just before the return block, around line 43:

Code language
JavaScript
// Tell 11ty to use the .eleventyignore and ignore our .gitignore file
config.setUseGitIgnore(false);

As the code comment says, we’re telling Eleventy to use .eleventyignore, so let’s configure that. Create a new file in your root eleventy-from-scratch folder called .eleventyignore and add the following to it:

Code language
text
node_modules

What we’re saying to Eleventy here is: “You can access whatever you like: just ignore the node_modules folder”.

Critical CSS permalink

There’s one CSS file that appears on every page as critical CSS. Let’s create that.

We need to create a folder for our Sass, though, so inside eleventy-from-scratch, run the following command in your terminal:

Code language
bash
mkdir src/scss

Now, in your new scss folder, create a file called critical.scss and add the following to it:

Code language
scss
@import 'reset';

We need to create this file for critical.scss to import, so create a new file in the same folder called _reset.scss and add the following to it:

Code language
scss
// A modified version of my "modern reset" https://github.com/hankchizljaw/modern-css-reset

/* Box sizing rules */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Remove default padding */
ul[class],
ol[class] {
  padding: 0;
}

/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul[class],
ol[class],
figure,
blockquote,
dl,
dd {
  margin: 0;
}

/* Set core root defaults */
html {
  scroll-behavior: smooth;
}

/* Set core body defaults */
body {
  min-height: 100vh;
  text-rendering: optimizeSpeed;
  line-height: 1.5;
}

/* Remove list styles on ul, ol elements with a class attribute */
ul[class],
ol[class] {
  list-style: none;
}

/* A elements that don’t have a class get default styles */
a:not([class]) {
  text-decoration-skip-ink: auto;
}

/* Make images easier to work with */
img {
  max-width: 100%;
  display: block;
}

/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
  font: inherit;
}

/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01s !important;
    scroll-behavior: auto !important;
  }
}

This is a slightly modified version of this CSS reset that I created. You can read about it in my article, A modern CSS reset.

You might have noticed that the file is called _reset.scss, rather than reset.scss. This is because Sass will ignore files with a _ prefix unless they are imported with @import.

Adding this task to our Gulpfile permalink

We’ve got our task, so now, let’s make Gulp aware of it. Open up eleventy-from-scratch/gulpfile.js and delete everything. Now, add the following:

Code language
JavaScript
const {parallel, watch} = require('gulp');

// Pull in each task
const sass = require('./gulp-tasks/sass.js');

// Set each directory and contents that we want to watch and
// assign the relevant task. `ignoreInitial` set to true will
// prevent the task being run when we run `gulp watch`, but it
// will run when a file changes.
const watcher = () => {
  watch('./src/scss/**/*.scss', {ignoreInitial: true}, sass);
};

// The default (if someone just runs `gulp`) is to run each task in parrallel
exports.default = parallel(sass);

// This is our watcher task that instructs gulp to watch directories and
// act accordingly
exports.watch = watcher;

What we do with this file now is import our task files (you could also write them directly in here) and then export a couple of different functions. Instead of writing module.exports like we have previously, we use exports.<functionName> instead. This is developer experience if nothing else. If we wanted to, we could write module.exports.watch, for example, but we’ll keep things as compact as possible.

Speaking of watch: along with that, we export default, too. These are functions that Gulp looks for when it is run. You might remember default from the last lesson. This is the function that’s executed when we run npx gulp in our terminal. If we run npx gulp watch, it will execute the function that we export with exports.watch. Say we wrote a function for exports.hello. If we ran npx gulp hello, that’s what would get run.

We’ve also use two really helpful Gulp functions that we imported at the start of the file: parallel and watch. With watch, we instruct it to watch a folder, and in most cases, a file-type in that folder. When something changes in that folder, the function—which in this case, is sass—is executed for us. We set ignoreInitial to true, because we’ll run our tasks initially in our default task, which coincidentally runs our parallel function.

The parallel function takes functions as the parameter and runs them all in parallel with each other. It then tells Gulp when they’re complete.

Updating our npm scripts permalink

Remember how we run npm start to build and serve the website? Let’s update those scripts to reflect our current setup.

Open up eleventy-from-scratch/package.json and delete everything inside the scripts element.

It should now look like this:

Code language
Diff - don’t copy
"scripts":{

}

Then, inside the scripts element, add the following:

Code language
json
"start": "npx gulp && concurrently \"npx gulp watch\" \"npx eleventy --serve\"",
"production": "NODE_ENV=production npx gulp && NODE_ENV=production npx eleventy"

Our start script now runs Gulp (exports.default) first. When that’s finished, it uses concurrently to run Gulp watch (exports.watch) and Eleventy together. Because we use concurrently, both Eleventy and Gulp work together as a team to keep things up to date as we change code. The reason we escape the quotes (\") is because they can be problematic for Windows users.

We also have production which first runs Gulp, then it runs Eleventy, all while NODE_ENV=production is set. This means that isProduction will return true, which allows our Sass task to compress the CSS output.

If it’s still running: stop Eleventy and then run the following in your terminal, while you’re in eleventy-from-scratch:

Code language
bash
npm install concurrently

Getting the CSS on the page permalink

We’ve got our Gulp task set up and working well, but now we need to get some CSS on the page.

We’re going to add two snippets to the base layout: base.html. Open up eleventy-from-scratch/src/_includes/layouts/base.html and just before the closing </head>, at around line 9, add the following:

Code language
HTML
<style>{% include "css/critical.css" %}</style>

{# Add facility for pages to delare an array of critical styles #}
{% if pageCriticalStyles %}
  {% for item in pageCriticalStyles %}
    <style>{% include item %}</style>
  {% endfor %}
{% endif %}

The first thing we do here is include our base critical CSS, which we’ve already discussed. The interesting bit comes after this, though.

In our actual page layouts, like work-item.html, we can declare pageCriticalStyles as an array. Inside that array, we can set paths to other critical stylesheets. We’ll demonstrate this later in the course, but what it allows us to do is break up our critical styles into smaller, more succinct pieces, which is great for performance.

Straight after this, in eleventy-from-scratch/src/_includes/layouts/base.html: add the following:

Code language
HTML
<link rel="stylesheet" media="print" href="/fonts/fonts.css?{{ assetHash }}" onload="this.media='all'" />

{# Add facility for pages to declare an array of stylesheet paths #}
{% if pageStylesheets %}
  {% for item in pageStylesheets %}
    <link rel="stylesheet" media="print" href="{{ item }}?{{ assetHash }}" onload="this.media='all'" />
  {% endfor %}
{% endif %}

These are our non-critical styles. None of these CSS files exist yet, but we’ll make them soon. We have a similar setup where a page can declare pageStylesheets which, again, we’ll be using soon.

The important thing to make a note of here is a little performance trick of setting the <link/>’s media attribute to "print". This tells the browser to still download the file, but deprioritise it, which means more important content loads first without being blocked (known as render blocking).

When the file has finished downloading, we have a tiny bit of JavaScript that converts the media attribute to all, which then allows the browser to parse it. If this JavaScript didn’t run, the loaded CSS would only be parsed if the browser printed something.

I urge you to read this post about it on the Filament Group Blog. It’s super smart!

Asset hashing permalink

Ok, last bit of this long lesson. When we add all of this CSS to the page, we’re referencing {{ assetHash }} a lot. We need to define that.

Open up eleventy-from-scratch/src/_includes/layouts/base.html and add the following right at the top of the file:

Code language
HTML
{% set assetHash = global.random() %}

Then, create a new file called global.js in your _data folder and add the following to it:

Code language
JavaScript
module.exports = {
  random() {
    const segment = () => {
      return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    };
    return `${segment()}-${segment()}-${segment()}`;
  }
};

This does exactly what it says on the tin: it returns a random string. This helps to keep our asset cache fresh because every time Eleventy re-builds, it will be a unique value. This means that every time you deploy your site, you can guarantee that your users won’t have out of date CSS.

When you run npm start in your terminal now, you should see something that looks like this:

The terminal saying that the site is ready to view at localhost 8080

Wrapping up permalink

Holy moly, this was a big one, right? The good news is that our Sass is set up, so writing CSS for this project will now be a joy.

Let’s move straight on to the next Gulp task.


Comments

Next up: Lesson 20 - Setting up fonts

Time to load our fonts from Google Fonts, locally.

Get started

Help! My version of this lesson isn’t working.

Don’t panic, you can download the working project here! Once they are downloaded: replace your project with these files and run npm install. You can read more information here.

Download project files

Hello, I’m Andy and I’ll help you build fast, accessible websites & design systems.

I’m a freelance CSS and design systems consultant, based in the UK. I specialise in design systems and creative web design, such as landing pages and campaign work.

I’m currently helping Google by refactoring the CSS and creating a design system for web.dev, but I have availability for projects such as small websites, landing pages and consultancy. I will have full availability for larger projects in January 2022.

Hire me