Lesson 19: Setting up Sass
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
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:
mkdir gulp-tasks
Then, create a file in your new gulp-tasks
folder called sass.js
and add the following to it:
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:
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:
// 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:
// 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
By default, Eleventy will ignore everything 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 .gitignore
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:
// 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:
node_modules
What we’re saying to Eleventy here is: “You can access whatever you like: just ignore the node_modules folder”.
Critical CSS
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:
mkdir src/scss
Now, in your new scss
folder, create a file called critical.scss
and add the following to it:
@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:
// 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
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:
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 parallel
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
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:
"scripts":{
}
Then, inside the scripts
element, add the following:
"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
:
npm install concurrently
Getting the CSS on the page
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:
<style>{% include "css/critical.css" %}</style>
{# Add facility for pages to declare 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:
<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 deprioritize 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
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:
{% set assetHash = global.random() %}
Then, create a new file called global.js
in your _data
folder and add the following to it:
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:
Wrapping up
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.