Learn Eleventy

Lesson 18: 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.

TIP

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 add a custom template language for Sass through Eleventy to convert our .scss files into .css files that the browser can parse and understand. This is called preprocessing.

Adding Sass as custom template language

The first thing we need to do is add a custom template language for Sass to the configuration in your eleventy.config.js file:

eleventyConfig.addExtension('scss', {
	outputFileExtension: 'css',
	useLayouts: false,
	compile: async function (inputContent, inputPath) {
		let parsed = path.parse(inputPath);
		// Don’t compile file names that start with an underscore
		if (parsed.name.startsWith('_')) {
			return;
		}

		const compiled = sass.compileString(inputContent, {
			loadPaths: [parsed.dir || '.', this.config.dir.includes],
			silenceDeprecations: ['import', 'global-builtin', 'slash-div']
		});

		// Map dependencies for incremental builds
		this.addDependencies(inputPath, compiled.loadedUrls);

		return async (data) => {
			return compiled.css;
		};
	},
});

eleventyConfig.addTemplateFormats('scss');
NOTE

You may have noticed that we configured our Sass compilation to ignore certain deprecation warnings with silenceDeprecations. These warnings tell us that certain Sass features will no longer work in future versions. The styling content of this course still relies on these features however, so we are ignoring them for now. Don't do this in the real world though! Make sure you are paying attention to deprecations and changes in the dependencies you use.

With addExtension, we register the scss file type and instruct Eleventy to use our compile function to process Sass files. In the compile function, we first ignore Sass partials that begin with an _ in the filename. Next, when we compile the result to a string, we make sure to load dependencies and partials in the same directory as the file being processed. Last but not least, we mark imported files as dependencies to Eleventy for incremental builds, and return a function that returns in the resulting CSS.

This uses two new dependencies, sass and Node.js's path module, which need to be imported at the top of the file, above export default function (eleventyConfig) {:

import path from 'node:path';
import * as sass from 'sass';

Lastly for this section, we have a new dependency to install. In your terminal, run the following:

npm install sass@v1.97.1

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 styles, though, so inside eleventy-from-scratch, run the following command in your terminal:

mkdir src/css

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

@import 'reset';

Since we are going to be including our critical styles inline on our pages, we don't need them to be generated in the output directory. In your css folder, create a file called css.11tydata.js and add the following inside:

export default {
	permalink: function (data) {
		// Don't write our critical included styles to the output directory
		const criticalStyles = ['critical', 'home', 'page', 'work-item'];
		if (criticalStyles.includes(data.page.fileSlug)) {
			return false;
		}
	},
};

Like our directory data files work.json for the work/ collection and posts.json for the posts/ collection, Eleventy will apply this JavaScript Data File to the entire folder because css.11tydata.js and the css/ parent folder share the same "file slug" (the css part before the file extensions).

In this case, our css.11tydata.js directory data file disables the permalink property for the hard-coded critical files, telling Eleventy to skip writing these files to the output directory.

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/Andy-set-studio/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.

TIP

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.

Getting the CSS on the page

We’ve got our Sass processing set up, but now we need to get some CSS on the page. Eleventy comes with the official Render plugin for rendering template files inside other templates, which we'll use to copy our compiled critical CSS into our layout.

Nothing to install here, but we'll need to import the plugin at the top of our eleventy.config.js configuration file:

import { RenderPlugin } from "@11ty/eleventy";

Within the config function, below the line which added the RSS plugin, add the newly-imported render plugin:

eleventyConfig.addPlugin(RenderPlugin);

Next, 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 8, add the following:

<style>{% renderFile "./src/css/critical.scss" %}</style>

{# Add facility for pages to declare an array of critical styles #}
{% if pageCriticalStyles %}
	{% for item in pageCriticalStyles %}
		<style>{% renderFile "./src/css/" + 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:

export default {
	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.

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 part of our asset pipeline — fonts.