Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 184 of X: SASS Basics (CSS)

09 May 2026

In the previous instalment, we dipped our toes into customising Bootstrap using just CSS ‘variables’ (CSS custom properties) on the client side. In doing so, we discovered that while it can be very useful to customise Bootstrap in this way, the approach is limited. We ended by pointing out that the primary, and most powerful, mechanism for customising Bootstrap remains SASS. Some months ago, during the initial part of our exploration of the static-site-generator Jekyll, we learned that SASS is a built-in feature, so any Jekyll site can utilise SASS without any extra effort. Putting all that together, SASS is clearly a technology worth exploring!

This instalment is intended to give a basic overview of how SASS works. The aim is to learn enough to start making valuable use of the technology, but this is not going to be anywhere near deep-dive into the full feature set!

Ideally, this instalment will whet your appetite to start making use of SASS in your projects, teach you enough to get started with it, and arm you with a sufficient understanding of the concepts and jargon to allow you to learn more as you need to.

This instalment is going to be a game of two halves — first, a whistle-stop tour of all the features with just simple, feature-specific, examples, then a realistic worked example that ties all the features together into a coherent whole.

Matching Podcast Episodes

Note that in its written form, this instalment is presented as a single unit — it tells a coherent story, and it would lessen its effectiveness to break it into two parts. However, there is too much content here for a single podcast episode, so this single post will appear on the podcast as Instalments 184a & 184b.

PBS 184a

You can also Download the MP3

Read an unedited, auto-generated transcript with chapter marks: PBS_2026_05_09

PBS 184b

When recorded will start at Worked Example — A Multi-Section Web Page. You can read ahead if you, like but it’s not proofread yet!

SASS — Syntactically Awesome Style Sheets

The reason SASS exists is to speed up the creation of regular cascading style sheets (CSS). It does this by providing features missing from CSS itself, allowing you to write clearer, more expressive, and more concise code. Browsers can’t interpret SASS, so the final step is a compiler that converts SASS code to regular CSS code. This is why SASS is referred to as a CSS preprocessor.

SASS was not the first or the only CSS preprocessor, but it’s the one that came out on top. Almost all the others have faded away into obscurity, with perhaps one small exception — LESS (Leaner Style Sheets). Less is still used and still developed today, but it’s much less popular than SASS, and in my opinion, that’s for good reasons, it’s just not as capable!

Using SASS is simple:

  1. Write your style sheets in SASS
  2. Compile your SASS stylesheet to CSS
  3. Import the compiled CSS into your HTML files in the usual way (using a <link rel="stylesheet"> tag in the <head>, or an @import at-rule in a <style> tag).

For historical reasons (so-called technical debt), the SASS preprocessor accepts two different syntaxes — the original indent-based SASS syntax, and the newer curly-brace-based SCSS (Sassy CSS) syntax. The newer SCSS syntax is a superset of regular CSS, so developers generally find it easier to learn. As well as that, it’s also more robust when copying and pasting because it uses curly braces to group statements rather than relying purely on indentation.

We will only be using the newer SCSS syntax in this series!

Yes, this is very confusing — we’re going to be using a piece of software named SASS that supports two syntaxes, one of which is also named SASS and saved in .sass files, while to the other is named the SCSS syntax and saved in .scss files, both of which are referred to as SASS files, and both of which can be converted to regular CSS with the sass command 🤯

To keep things as un-unclear as possible, we’re going to utterly ignore the older SASS syntax when writing our SASS files, and write all our SASS in the newer SCSS syntax!

From this sentence on we’ve going to completely forget the old obsolete SASS syntax ever existed! We’re going to make peace with the fact that our SASS files will have the .scss file extension, and we’re going to refer to our SCSS code as ‘SASS code’.

The SASS Basics

Firstly, I want to reiterate that we must save our SASS code in files with the .scss file extension!

Secondly, I want to stress that because SASS is a superset of CSS, all valid CSS code is valid SASS code!

Finally, since the defining characteristic of a superset is that it contains the original set and more, SASS adds additional features to the CSS syntax, though it does so in a very CSS-like way.

Here’s a very quick overview of the SASS-specific features we’ll be exploring, and a short explanation of their usefulness:

The Atoms of a SASS Style Sheet

Before we look at any SASS syntax, let’s start by describing the jargon used to describe the various SASS features. A basic understanding of these terms is important for understanding both the rest of this instalment, and the official documentation.

Because SASS was designed as a superset of traditional CSS, a lot if the jargon is already familiar to us from our use of CSS throughout this series.

With all that said, these are the proverbial Lego bricks SASS style sheets are built from:

  1. CSS Style Declarations — definitions for style properties applied to different parts of an HTML document with selectors.
  2. CSS Style Properties — the named pieces of styling information CSS uses to control the way HTML elements rendered — the colours to use, how much padding there should be around an element, whether or not there should be a border, the typography to use for the text, and so on.
  3. Selectors — the CSS syntax for defining the subsets of elements on a page style the declarations get applied to. As a quick reminder, the selector p applies declarations to all paragraphs, the selector #waffle applies declarations to just the element with the ID waffles, and the selector p.pancakes applies declarations to all paragraphs with the class pancakes.
  4. Custom CSS Properties — also known as CSS Variables, and the subject of instalment 183.
  5. SASS Variables — more traditional variables provided by SASS. SASS variables only exist in your SASS code; they get distilled to CSS values at compile time by the SASS compiler, so they just look like ordinary values in the generated CSS.
  6. SASS Operators — a small set of the usual mathematical and boolean operators like ==, !=, +, - etc. which you can use in your SASS code to process the values in SASS variables and the outputs from SASS functions.
  7. CSS at-rules — CSS statements starting with an @ that perform special actions, that is to say, they don’t declare a style, but do something else instead. The best examples we’ve seen in this series are @property for defining CSS custom properties and @import for importing external resources like the web fonts published via Google Fonts.
  8. SASS at-rules — special SASS statements that look like CSS at-rules but only exist in SASS. These SASS at-rules provide SASS-specific functionality. Whatever it is these statements do, they all get converted to regular CSS code by the SASS compiler in some way. Notable examples we’ll be exploring are @extend for style inheritance, @if for conditionals, and @each for looping.
  9. CSS Functions — named computations and transformations that take arguments and convert them to values that can be used in style declarations. These are a standard part of CSS, so the SASS compiler does not execute them, but simply passes them through to the generated CSS code. It’s the browser that executes these functions when it renders the page, not the SASS compiler.
  10. SASS Functions & Modules — SASS functions look like CSS functions, but like SASS at-rules, they only exist in SASS, so they will never get passed through to the generated CSS. Rather than being executed by the browser like traditional CSS functions, SASS functions are executed by the SASS compiler. What appears in the generated CSS is the values returned by these functions. Rather than simply adding all functions into a global namespace, SASS groups the functions it provides into separate namespaces known as modules. To use a function you need to load the module that provides it with the @use SASS at-rule.
  11. Mixins — named re-usable templates that can accept arguments, and generate CSS style declarations. Think of them like a very task-specific version of the Mustache templates we used for our Javascript web apps earlier in this series.
  12. SASS Modules — named collections of SASS functions, variables, and mixins. They add a little order to things, and avoid cluttering the global namespace.

Data Types in SASS

SASS variables store data, and SASS functions process and return data, so let’s take a moment to describe how that data is represented.

At this stage, you’ve probably noticed a pattern — SASS minimises inventing its own concepts and inherits as many concepts as it can from CSS, then expands them when needed. This pattern holds for data types.

All valid CSS data types are valid SASS data types. In other words, SASS can handle colours like #c0c0c0 & DarkGray, dimensions like 45px, 24% & 3rem, bare keywords like solid, numbers like 42 & 3.1415, and strings enclosed in single or double quotes (whichever is most convenient in any situation).

What SASS adds to the mix are two important data types CSS does not support — lists and dictionaries.

The official SASS jargon refers to what we call lists as Lists (convenient for us!), and what we call dictionaries as Maps. This is actually a nice term for dictionaries because they provide mappings between names and values. It’s also a term we’ve seen before, in our exploration of jq!

Comments — SASS+CSS or SASS-Only

SASS supports two types of comments, CSS comments, and SASS comments. While neither get executed by the SASS compiler, it does treat them differently.

Standard CSS comments (/* … */) in SASS files get passed through to the generated CSS, so they remain visible after compilation.

SASS comments (// …) on the other hand, get swallowed by the SASS compiler, and do not appear in the generated CSS.

/* This is a comment in both SASS and CSS, and it will appear in the generated CSS */
/* Use comments like this to annotate the generated CSS */

// This on the other hand is a SASS comment, it will not appear in the generated CSS
// Use comments like this to describe SASS-specific aspects of your code

Useful Resources

Installing the sass Command

Many static site generators like Jekyll build SASS compilation into their own code, allowing them to process SASS to CSS as part of their standard processes. We’ll be making use of this built-in support when we return our focus to Jekyll later this year, but for this instalment, we need to do our own SASS compilation. That means we need to install the sass command line tool.

There are multiple versions of this tool available — both traditional OS-specific binaries, and portable versions for cross-platform environments like NodeJS. You can find the full list on the official install page.

In this instalment we’ll use the NodeJS version. there are two reasons for this — we’ve already used NodeJS repeatedly throughout this series, and it’s cross-platform, so everyone can play along!

Assuming you have the latest stable version of NodeJS installed, let’s install the NodeJS SASS package in NodeJS’s global scope — that is to say, at the system level, rather than at a local folder level like we normally do when working on specific projects:

sudo npm install -g sass

Once you have the NodeJS version of sass installed you can compile SCSS files to CSS files with commands of the form:

npx sass source_file.scss output_file.css

Note that the npx prefix is only needed because we’re using the NodeJS version of SASS. If you install an OS-specific binary version, you omit the npx in all the examples in this instalment. Remember, npx is NodeJS’s mechanism for finding and executing commands provided by installed modules, I think of it as standing for ‘Node package execute’.

Compiling Simple Examples

When we get to our worked example we’re going to be compiling SASS code to create CSS code that will be applied to an HTML file. But before we’re ready for that, we need to quickly compile small snippets of SASS code to see how they get translated to CSS. For this purpose it’s more convenient to have the generated CSS appear straight in the terminal instead of being written to a file. We can do that by executing the sass command in so-called STDIN mode with the --stdin flag. We can then use terminal redirection to send a temporary .scss file with our sample code into the command using the shell’s < input redirection operator:

npx sass --stdin --no-source-map < temp.scss

Ignore the --no-source-map flag for now, we’ll explain map files later.

Let’s test this by creating a file named temp.scss with the following content:

/* Hello Generated CSS World!
 * ==========================
 * This is a CSS style sheet that does absolutely nothing but was generated from a SASS file.
 */

// Hello SASS World!
// =================
// This is a SASS comment so it won't be visible in the generated CSS!

Open a terminal in the same folder you saved this temp file to, and generate the CSS by running the command:

npx sass --stdin --no-source-map < temp.scss

If everything is working correctly, the only thing output to the terminal should be the generated CSS, which should look like this:

/* Hello Generated CSS World!
 * ==========================
 * This CSS style sheet that does absolutely nothing was generated from a SASS file.
 */

This simple little test verifies that you’ve successfully installed sass. It also illustrates the difference between CSS and SASS comments — the CSS comment appeared in the generated CSS, but the SASS comment didn’t.

A Quick SASS Overview

To prepare for our worked example, let’s run through the SASS features we’ll be covering with simple little examples to illustrate each.

Selector Nesting

One of SASS’s most useful features is the ability to nest style declarations. In regular CSS, when you want to target a suite of styles to parts of a page contained within other parts of the page, you need to duplicate the nesting for each selector.

Imagine you have block quotes with the class .aside that need custom styles for the block quotes themselves, headings inside those block quotes, and paragraphs inside these block quotes. We would need to define our styles using three separate selectors, each starting at the root of the document and defining the full desired nesting. The general shape of the code would be:

/* style asides */
blockquote.aside {
  /* styles that apply to the block quotes themselves go here */
}
blockquote.aside h1 {
  /* styles that apply to headings inside block quotes go here */
}
blockquote.aside p {
  /* styles that apply to paragraphs inside block quotes go here */
}

This works just fine, but it’s not great for developers for two reasons:

  1. The repetitive selectors are tedious to write.
  2. The specified nesting is not obvious at a glance.

SASS gives us a much simpler option — we can nest style definitions! In SASS the CSS code above becomes:

/* style asides */
blockquote.aside {
  /* styles that apply to the block quotes themselves go here */
  
  h1 {
    /* styles that apply to headings inside block quotes go here */
  }
  
	p {
    /* styles that apply to paragraphs inside block quotes go here */
  }
}

This is easier to write, and, the intended meaning is much clearer. The SASS compiler simply builds the CSS selectors for us. Save the code snippet above in temp.scss and compile it to CSS with the command:

npx sass --stdin --no-source-map < temp.scss

This should output the following CSS:

/* style asides */
blockquote.aside {
  /* styles that apply to the block quotes themselves go here */
}
blockquote.aside h1 {
  /* styles that apply to headings inside block quotes go here */
}
blockquote.aside p {
  /* styles that apply to paragraphs inside block quotes go here */
}

Notice it’s identical to the original CSS snippet.

Style Inheritance with @extend

It’s quite common to have an existing style defined for one selector that you’d like to use as the basis for another selector. You can, of course, do this kind of thing with regular CSS, but like with selector nesting, the code is difficult to write, read, and debug.

Imagine we have a style for block quotes, but we want to add some flexibility to our style sheet and allow authors to encode quotations either with <blockquote> tags or with paragraph tags with the quotation class, i.e. <p class="quotation">.

Initially we think we want the styling to be identical, so we create one style declaration that uses both selectors:

/* style quotations, regardless of how they are marked up */
blockquote, p.quotation {
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 3rem;
  font-size: 1.25rem;
}

When we do this we notice that our authors actually use the two types of quotation differently, for pull quotes that are used to signpost the article’s themes when skimming, they use <blockquote> tags, but for quotations that form part of the narrative they use <p class="quotation"> tags.

We now want to have these stored types of quotations to be similar, but distinguishable. The inline quotations are the standard, so we keep their style as:

/* style inline quotation paragraphs */
p.quotation {
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 3rem;
  font-size: 1.25rem;
}

And we want the pull quotes to be similar, but to have more padding and a dimmer font so they fade out a little. As a first draft we copy-and-paste our first style and tweak it to give:

blockquote {
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 5rem;
  font-size: 1.25rem;
  color: DarkGray;
}

This is bad code — look at that duplication! If we want to tighten or relax our paddings we have to remember to tweak them in two places!

The best practice in plain CSS is to apply the common styles to both selectors in one style declaration, and to override and augment as needed in a separate definition for the more specific of the two selectors. In this case, we have a selector for a bare tag (blockquote) and a selector for a tag with a class (p.quotation). A tag and a class is more specific, so that’s the selector that needs to be used for overriding and extending:

blockquote, p.quotation { /* Note the double selector */
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 3rem;
  font-size: 1.25rem;
}
p.quotation { /* must be the more specific selector */
	padding: 5rem; /* override from the declaration above */
  color: DarkGray; /* addition beyond the declaration above */
}

This works, but we had to figure out which of our selectors had the highest specificity, which is more mental work for us, and it opens the door to more bugs.

With SASS we can better express our intention by defining the block quote style in one block, and the style for quotation paragraphs in another which explicitly inherits from the the first:

blockquote { // note the single selector
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 3rem;
  font-size: 1.25rem;
}
p.quotation {
  @extend blockquote; // explicit inheritance
	padding: 5rem; /* override */
  color: DarkGray; /* add */
}

Saving this snipped to temp.scss and compiling it with:

npx sass --stdin --no-source-map < temp.scss

Will produce output that looks the same as our original CSS:

blockquote, p.quotation {
  margin-top: 0px;
  margin-bottom: 1rem;
  padding: 3rem;
  font-size: 1.25rem;
}

p.quotation {
  padding: 5rem; /* override */
  color: DarkGray; /* add */
}

Simple SASS Variables

Unlike CSS custom properties, SASS variables really do behave like the typical variables in regular programming languages. Their scope is even (mostly) defined by curly braces (more on that later).

Variables must be named with a $ prefix, and they get assigned values using the same syntax as style declarations. So, for example:

$my-variable: "Hello World!";

By convention, they are named in all lower-case with and dash-separated, like CSS properties.

Unlike CSS custom properties, you don’t need to do anything special to access a variable — just use its name as a value:

$heading-color: #00408d;

h1 {
  color: $heading-color;
}

Basic Math & String Operators

When working with strings or numbers, it can be useful to perform basic operations like arithmetic and concatenation. SASS doesn’t provide many operators, but it does provide most of the usual suspects:

So what’s missing? There is no division operator — or rather, there was one, but it’s now deprecated, so you should pretend there isn’t!

Does that mean you can’t divide in SASS? Not really, but it does mean that the syntax for division is awkward. So awkward in fact, that it’s often much simpler to avoid division by using the matching multiplication instead. For example, rather than divide by 2, simply multiply by 0.5! Personally, I rarely resort to division because usually I’m just looking for a simple fraction, and multiplication works just as easily!

But you can of course divide when you really need to, how? By using a the Math module!

Advanced Math (and Division!) with the sass:math Module

Like with every SASS module, you need to tell the compiler to load the Math module (sass:math) before you can use it. You do this by adding the appropriate @use SASS at-rule to the top of your code. To use the Math module that means you add:

@use "sass:math";

This module provides a lot of variables and functions, and you’ll find all the details in the module’s documentation. Here are some of the highlights:

Interpolation with #{}

As we’ve seen, to use a variable’s value as a value for a style declaration you don’t have to do anything more than use its name.

But you can do more with variables than that! You can actually use SASS variables just about everywhere (see the documentation for full details)! The complication is that when using them in any context other than a CSS value, you need to use the interpolation operator (#{}).

One of the most common places to see interpolation used is in CSS style property names. As an example, let’s use variables to add a border to block quotes that will have a default border style on three sides, and then a different-width border on one side, but we want the side to be configurable. That means we need to vary the CSS property name itself, not the value it gets assigned. CSS provides the property names border-top, border-right, border-bottom & border-left, so we want to use a variable to store one of top, right, bottom & left, and have that variable control the side that gets treated differently (which we’ll name $border-highlight-side):

$border-color: #00408d;
$border-style: solid;
$border-width: 1px;
$border-highlight-side: 'left'; // the side of the border to make different
$border-highlight-width: 5px;

blockquote {
  border: $border-width $border-style $border-color;
  border-#{$border-highlight-side}: $border-highlight-width $border-style $border-color;
}

This compiles to the following CSS:

blockquote {
  border: 1px solid #00408d;
  border-left: 5px solid #00408d;
}

Notice that the property name with interpolation border-#{$border-highlight-side} became border-left, showing the value of a variable being used as part of a CSS property name rather than as a value!

There are two other important things to note about interpolation in SASS:

  1. You can use interpolation inside strings — this can be particularly useful for debugging, letting you add the value of a variable to a string.
  2. You can use the interpolation syntax to insert any value not just the value from a variable. Most importantly, interpolation works with functions!

Debugging with the @debug SASS At-Rule & sass:meta Module

Once you start using variables in your SASS code it’s only a matter of time until you find yourself wanting to peer inside a variable to see its value. How else are you going to get to the bottom of that annoying bug that’s driving you nuts‽

SASS offers a few useful tools for this, the most important of which is the @debug SASS at-rule.

You can include lines of the form @debug any_value; (e.g., @debug $my-variable;) anywhere in your code, and when you compile the SASS code, the debug message will be written to the standard error stream on your terminal, prefixed with DEBUG: and annotated with a line number. This tells you both what the value was and where in your code it had that value.

For basic data types, @debug may be all you need, but as you start to use more advanced types like lists and maps, you’re likely to want a little more information about your variables. This is where the suite of optional functions made available through the standard sass:meta module comes in.

The sass:meta modules contains a lot of functions, many of which are quite advanced, so we’re going to limit ourselves to the ones that are most useful for debugging. But, if you’re curious, or looking for something specific, the module’s documentation has all the details.

Like with every SASS module, you need to tell the compiler to load it for you by adding an appropriate @use SASS at-rule to the top of your code. For the sass:meta module you need to add:

@use "sass:meta";

These are the most useful functions for debugging:

More Advanced SASS Variables (Lists & Dictionaries)

Lists

SASS lists are just lists of multiple values, like arrays in traditional programming languages. While the concept is similar, the syntax is unusually flexible. You can use the most convenient syntax depending on what looks the clearest, and whether or not you need nesting.

Here are all the ways of declaring a list:

$my-space-delimited-bare-list: item1 item2 item3;
$my-comma-delimited-bare-list: item1, item2, item3;
$my-space-delimited-braced-list: (item1 item2 item3);
$my-space-delimited-squre-bracketed-list: [item1 item2 item3];
$my-nested-list: ([list1-item2 list1-item2], [list2-item1 list2-item2]); // like a 2D array

Lists can be used as values for CSS properties that accept multiple values, like font-family.

An interesting quirk is that lists are one-indexed rather than zero-indexed — the first item in a list has the index 1!

SASS provides a number of useful list-related functions via the sass:lists module. As with all modules it has to be loaded at the start of your code with with:

@use 'sass:list';

The full list of functions is described in the module’s documentation, but let’s use an example to explore the most useful ones.

Save the following to a file named temp.scss :

@use 'sass:list';
@use 'sass:meta';

// define a sample list
$desserts: waffles, pancakes, ice cream;

// get the number of elements
@debug "There are #{list.length($desserts)} desserts!";

// access specific elements in the list:
@debug "First dessert: #{list.nth($desserts, 1)}";
@debug "Second dessert: #{list.nth($desserts, 2)}";
@debug "Last dessert: #{list.nth($desserts, -1)}";

// add a dessert to the list
$desserts: list.append($desserts, cake); // note the assignment back to $desserts!
@debug "Updated list: #{$desserts}";

// find the index for cake
@debug "Cake is at index #{list.index($desserts, cake)}";

// join arrays
$british-desserts: scone, crumpet;
$desserts: list.join($desserts, $british-desserts); // note the assignment back to $desserts!

// inspect an array
@debug "The final list: #{meta.inspect($desserts)}";

Since we don’t actually need the generated CSS, we can use a little terminal trickery to compile the file, generate the debug messages, and then redirect the output into nothingness:

npx sass --stdin --no-source-map < temp.scss > /dev/null

This produces the following output:

-:8 DEBUG: There are 3 desserts!
-:11 DEBUG: First dessert: waffles
-:12 DEBUG: Second dessert: pancakes
-:13 DEBUG: Last dessert: ice cream
-:17 DEBUG: Updated list: waffles, pancakes, ice cream, cake
-:20 DEBUG: Cake is at index 4
-:27 DEBUG: The final list: waffles, pancakes, ice cream, cake, scone, crumpet

I want to draw your attention to one very important detail — lists are immutable!

That is to say, lists cannot be edited once they are created. This means that list-altering functions like list.append() & list.join actually create entirely new lists, which is why we need to explicitly save them back to a variable!

Maps (Dictionaries)

The concept we refer to as dictionaries in this series (key-value pairs) are available as maps in SASS.

The syntax for SASS maps is a lot less flexible than the syntax for lists — there is just one way to declare a map:

$my-map: ("key1": value1, "key2": value2);

One thing to note about the syntax is that is can be spread over multiple lines, which is the convention for anything but a very small map.

Like with lists, SASS provides functions for working with maps via a module, and again, like with lists, we need to load it by adding the following to the start of our code:

@use "sass:map";

Again, like with lists, the module provides more functions that we need, and you can find the details in the module’s documentation.

Let’s see some of the most useful map functions in action.

Save the following to temp.scss:

@use "sass:map";
@use "sass:list";
@use 'sass:meta';

// define a pair of maps that map weight names to weight values
$weight-names: ( // the official names
  "thin": 100,
  "extra-light": 200,
  "light": 300,
  "normal": 400,
  "medium": 500,
  "semi-bold": 600,
  "bold": 700,
  "extra-bold": 800,
  "black": 900
);
$weight-name-aliases: ( // common alternative names for weights
  "ultra-light": 200,
  "regular": 400,
  "demi-bold": 600,
  "ultra-bold": 800,
  "heavy": 900
);

// get the keys from a map
@debug "The canonical weights are: #{meta.inspect(map.keys($weight-names))}";

// get the values from a map
@debug "There are aliases for the following values: #{meta.inspect(map.values($weight-name-aliases))}";

// get the size of a map (number of key-value pairs)
@debug("There are #{list.length(map.keys($weight-names))} canonical weights");

// check if a key exists in the map
@debug("Is 'demi-bold' a canonical weight? #{map.has-key($weight-names, 'demi-bold')}");
@debug("Is there an alias 'demi-bold'? #{map.has-key($weight-name-aliases, 'demi-bold')}");

// access the value for a key
@debug "The typographic term 'semi-bold' maps to font-weight: #{map.get($weight-names, 'semi-bold')}";

// add a key-value pair
$weight-names: map.set($weight-names, 'regular', 400);

// merge two maps — key confilicts OK, value from second map used
$weight-names-all: map.merge($weight-names, $weight-name-aliases);

// inspect a map
@debug "All weights & aliases: #{meta.inspect($weight-names-all)}";

Running this with the same command we used for lists (npx sass --stdin --no-source-map < temp.scss > /dev/null) produces the following output:

-:26 DEBUG: The canonical weights are: "thin", "extra-light", "light", "normal", "medium", "semi-bold", "bold", "extra-bold", "black"
-:29 DEBUG: There are aliases for the following values: 200, 400, 600, 800, 900
-:32 DEBUG: There are 9 canonical weights
-:35 DEBUG: Is 'demi-bold' a canonical weight? false
-:36 DEBUG: Is there an alias 'demi-bold'? true
-:39 DEBUG: The typographic term 'semi-bold' maps to font-weight: 600
-:48 DEBUG: All weights & aliases: ("thin": 100, "extra-light": 200, "light": 300, "normal": 400, "medium": 500, "semi-bold": 600, "bold": 700, "extra-bold": 800, "black": 900, "regular": 400, "ultra-light": 200, "demi-bold": 600, "ultra-bold": 800, "heavy": 900)

I want to draw your attention to two things:

  1. To fully use maps we also need lists, so it’s quite normal to see both modules used together. This makes sense when you consider that the keys for a map are a list of strings.
  2. Like lists, maps are immutable, so the functions that alter them like list.set() and list.merge() return entirely new maps that need to be explicitly saved to a variable if they need to be used again later in your code.

Conditionals with the @if & @else SASS At-Rules

SASS allows styles to be applied conditionally by wrapping the relevant code in @if and optionally also @else blocks. The syntax is simply:

@if some-condition {
  // will happen when the condition is truthy
} @else {
  // will happen when the condition is not truthy
}

The condition can be anything that produces a value. If that value is a Boolean then it’s used directly, if not, then SASS passes the value through it’s truthiness logic.

Both @if & @else are exceptions when it comes to variable scope — the variables inside the curly braces associated with these two at-rules do not get their own scope, they remain in the same scope as the variables outside these blocks.

‘Truthiness’ in SASS

SASS is extremely restrictive on what it considers falsey — there are exactly two values that evaluate to false in SASS — false and null. Literally everything else, including empty strings, empty lists, empty maps, and even 0, evaluate to true.

Yes, in SASS, 0 and empty strings are true! Really!

Boolean & Comparison Operators

Like with the arithmetic operators, SASS provides the basics, but nothing more (though without major omissions in this case).

There are the usual Boolean operators — and, or, and not.

And then there are the comparison operators:

And that’s all there is!

An Example Conditional Style

To see how conditionals can be used to conditionally include some style definitions, save the following as temp.scss:

// A global variable to optionally minimise borders throughout the style sheet
$minimise-borders: true;

// …

/* Style block quotes */
blockquote {
  margin-top: 0px;
  margin-bottom: 1rem;
  @if not $minimise-borders {
    border: 1px solid black;
  } @else {
    /* border suppressed by SASS variable $minimise-borders */
  }
}

Compiling this with the command:

npx sass --stdin --no-source-map < temp.scss

Generates the following CSS:

/* Style block quotes */
blockquote {
  margin-top: 0px;
  margin-bottom: 1rem;
  /* border suppressed by SASS variable $minimise-borders */
}

Now, change the line $minimise-borders: true; to $minimise-borders: false; and repeat, now we get the following CSS:

npx sass --stdin --no-source-map < temp.scss
/* Style block quotes */
blockquote {
  margin-top: 0px;
  margin-bototm: 1rem;
  border: 1px solid black;
}

Three things to note:

  1. Because CSS does not support @if or @else, their presence in the SASS code is completely invisible in the generated CSS — the SASS compiler processes these SASs-specific at-rules down to valid CSS.
  2. When we configured our SASS variable to minimise borders, the generated CSS had no border, just the comment defined in the @else block. When we configured our SASS variable not to minimise borders, the generated CSS added a border as per the @if block.
  3. The SASS code was properly indented to show the presence of the conditional, but that extra indentation is removed by the SASS compiler, generating perfectly indented CSS code.

Loops with the @each & @for SASS At-Rules

SASS provides two distinct looping constructs for achieving two distinct ends:

  1. @each loops over a list or map
  2. @for repeats an action a specific number of times

All looping at-rules are exceptions when it comes to variable scope — the variables inside the curly braces associated with these at-rules do not get their own scope, they remain in the same scope as the variables outside these blocks.

SASS also provides an @while at-rule, but the documentation literally says to avoid using it unless absolutely necessary!

Looping Over Lists with @each

If you have a list, you can execute some code once for each item in the the list with the following syntax:

@each $item-name in $list {
  // This happens once for each item in the list
  // The current item will be avaiable via $item-name
}

Note that we get to choose the name the item will have on each pass through the loop — $item-name is a placeholder, it could be anything, even $waffle 😉

As an example, save the following as temp.scss:

$desserts: waffle, pancake, cake;

@each $dessert in $desserts {
  @debug $dessert;
}

Then compile it with npx sass --stdin --no-source-map < temp.scss > /dev/null. The output will be:

-:4 DEBUG: waffle
-:4 DEBUG: pancake
-:4 DEBUG: cake

Looping Over Maps with @each

If you have a map, you use the same at-rule used for lists, but you provide two names rather than one — a name for each key, and a name for each matching value:

@each $key-name, $value-name in $map {
  // this happens once for each key-value pair in the map
  // the current key will be available via $key-name, and the matching value via $value-name
}

Again, note that both $key-name and $value-name are placeholders, you can use any names that makes sense in context.

As an example, save the following as temp.scss:

$weight-names: ( // the official names
  "thin": 100,
  "extra-light": 200,
  "light": 300,
  "normal": 400,
  "medium": 500,
  "semi-bold": 600,
  "bold": 700,
  "extra-bold": 800,
  "black": 900
);

@each $weight-name, $weight in $weight-names {
  @debug "The weight '#{$weight-name}' corresponds to font-weight: #{$weight}";
}

Again, compile this with npx sass --stdin --no-source-map < temp.scss > /dev/null and you should see the following output:

-:14 DEBUG: The weight 'thin' corresponds to font-weight: 100
-:14 DEBUG: The weight 'extra-light' corresponds to font-weight: 200
-:14 DEBUG: The weight 'light' corresponds to font-weight: 300
-:14 DEBUG: The weight 'normal' corresponds to font-weight: 400
-:14 DEBUG: The weight 'medium' corresponds to font-weight: 500
-:14 DEBUG: The weight 'semi-bold' corresponds to font-weight: 600
-:14 DEBUG: The weight 'bold' corresponds to font-weight: 700
-:14 DEBUG: The weight 'extra-bold' corresponds to font-weight: 800
-:14 DEBUG: The weight 'black' corresponds to font-weight: 900

Looping over Ranges with @for

In SASS, @for loops are effectively a short-cut for the most common types of counting loops — those that go from an initial value to a final value in increments of one. You tell @for where to start and where to end, and it will move between those two numbers by adding or subtracting 1 each time (depending on whether the first number is larger than the second or vice-versa).

The other thing you have to tell @for is whether to loop inclusively, or exclusively, of the final number. If your numbers are 1 and 5 do you want to repeat the code for 1, 2, 3, 4 & 5, or just for 1, 2, 3 & 4? You express your intention by choosing one of two possibly keywords — through to include the last number, and to to exclude it. That gives two possible syntaxes:

// inclusive loops
@for $integer-name from $start-integer through $end-integer {
  // this happens once for each integer from the first up-to-and-including the last
  // each time the current number will be available as $number-name
}

// exclusive loops
@for $integer-name from $start-integer to $end-integer {
  // this happens once for each integer from the first up-to-but-not-including the last
  // each time the current number will be available as $number-name
}

There are three things to note here:

  1. Like with @each, the name $integer-name is a placeholder, it can be anything. By convention it’s usually simply $i though.
  2. The numbers must be integers
  3. The step size is always one

As an example, save the following as temp.scss:

@debug "Inclusive Loop from 1 through 5:";
@for $i from 1 through 5 {
  @debug " - #{$i}";
}

@debug "Exclusive Loop from 1 to 5:";
@for $i from 1 to 5 {
  @debug " - #{$i}";
}

Again, compile with npx sass --stdin --no-source-map < temp.scss > /dev/null and you should see the output:

-:1 DEBUG: Inclusive Loop from 1 through 5:
-:3 DEBUG:  - 1
-:3 DEBUG:  - 2
-:3 DEBUG:  - 3
-:3 DEBUG:  - 4
-:3 DEBUG:  - 5
-:6 DEBUG: Exclusive Loop from 1 to 5:
-:8 DEBUG:  - 1
-:8 DEBUG:  - 2
-:8 DEBUG:  - 3
-:8 DEBUG:  - 4

SASS Templates with @mixin & @include

When you need to create multiple similar style declarations, it can be very convenient to use some kind of template. That’s what the feature the SASS documentation refers to as mixins provides. You define what is, in effect, a style template with named arguments using the @mixin SASS at-rule, then you use it to generate style declarations with the @include SASS at-rule.

SASS mixins are often used in conjunction with loops to create suites of related CSS classes. This is how Bootstrap creates its many suites of similar related classes, like for example the .border-1.border-5 classes.

To define a mixin use the following syntax:

@mixin mixin-name($arg1-name: arg1DefaultValue) {
  // styles declarations here, making use of the argument names as desired
}

A few things to note:

  1. The list of arguments is entirely optional, but it’s rare not to need any at all. Should you just want to have some identical styles you can add anywhere, you can completely omit the () from the end of the mixin-name.
  2. The default values for arguments are optional, but arguments without default values are treated as required by the SASS compiler!
  3. To make an argument truly optional, so that it doesn’t even have a default value, explicitly assign it the default value null.

Once a mixin exists, it can be included in any style declaration by using the @include SASS at-rule. The syntax is:

// Include a mixin without arguments
@include mixin-name;

// Include a mixin with arguments
@include mixin-name($arg1-value, $arg2-value);

To see mixins in action, save the following as temp.scss:

@use 'sass:map';

// a mixin with no arguments that defines the standard spacing for blocks
@mixin standard-block-spacing {
  margin: 1rem;
  padding: 1rem;
}

// style regular paragraphs
p {
  @include standard-block-spacing;
  text-align: justify;
}

// block qotes
p.blockquote {
  @include standard-block-spacing;
  font-size: 1.25rem;
}

// a mixin that supports four arguments:
// - $font-size      : the size to make the heading text (REQUIRED)
// - $font-weight    : the weight to use for the heading (REQUIRED)
// - $colour         : an optional colour that defaults to black
// - $text-transform : a completely optional text transformation to apply
@mixin heading-text($font-size, $font-weight, $color: black, $text-transform: null){
  font-size: $font-size;
  font-weight: $font-weight;
  color: $color;
  // if a text transform was not passed it will be null, which is falsey
  @if $text-transform {
    text-transform: $text-transform;
  }
}

// define a map which maps heading tags to nested maps of header properties
$standard-headings: (
  "h1": (
    "font-size": 2rem,
    "font-weight": 800,
  ),
  "h2": (
    "font-size": 1.75rem,
    "font-weight": 600,
  ),
  "h3": (
    "font-size": 1.5rem,
    "font-weight": 500,
  ),
  "h4": (
    "font-size": 1.25rem,
    "font-weight": 400,
  )
);

// Loop over the supported headers to generate the needed styles with the help of the mixin
@each $heading-tag, $heading-details in $standard-headings {
  #{$heading-tag} {
    // apply the heading-text mixin
    @include heading-text(map.get($heading-details, 'font-size'), map.get($heading-details, 'font-weight'), DarkBlue);
  }
}

// create a special heading type for banner headlines, also with the help of the mixin
h1.banner {
  @include heading-text(5em, 200, Blue, capitalize);
}

To see the set of styles we’ve produced, let’s compile this SASS straight to the terminal (like we did earlier in the instalment), we can do that with the command:

npx sass --stdin --no-source-map < temp.scss

This shows our code generated the following CSS styles:

p {
  margin: 1rem;
  padding: 1rem;
  text-align: justify;
}

p.blockquote {
  margin: 1rem;
  padding: 1rem;
  font-size: 1.25rem;
}

h1 {
  font-size: 2rem;
  font-weight: 800;
  color: DarkBlue;
}

h2 {
  font-size: 1.75rem;
  font-weight: 600;
  color: DarkBlue;
}

h3 {
  font-size: 1.5rem;
  font-weight: 500;
  color: DarkBlue;
}

h4 {
  font-size: 1.25rem;
  font-weight: 400;
  color: DarkBlue;
}

h1.banner {
  font-size: 5em;
  font-weight: 200;
  color: Blue;
  text-transform: capitalize;
}

Notice that the two style declarations defined with our example no-argument mixin standard-block-spacing have been inserted into the style declarations for both <p> and <blockquote> tags.

Also notice that our loop over the nested map with heading details generated style declarations for each of our four defined heading tags, and that each one has the three style declarations from the heading-text mixin for the two required arguments and the one with a default value, but no value for the fourth completely optional argument.

Further, notice our use of interpolation on the left-side of the : produced the desired effect, defining styles for h1 to h4 from the keys in the map:

h1 {
  font-size: 2rem;
  font-weight: 800;
  color: DarkBlue;
}

h2 {
  font-size: 1.75rem;
  font-weight: 600;
  color: DarkBlue;
}

h3 {
  font-size: 1.5rem;
  font-weight: 500;
  color: DarkBlue;
}

h4 {
  font-size: 1.25rem;
  font-weight: 400;
  color: DarkBlue;
}

Finally, notice that our style definition for banner headings which passes the fourth completely optional text transformation argument to the header-text mixin has an extra style definition because the @if evaluated to true:

h1.banner {
  font-size: 5em;
  font-weight: 200;
  color: Blue;
  text-transform: capitalize;
}

Instalment Resources (Only Needed for Part B)

Worked Example — A Multi-Section Web Page

In order to see SASS in action let’s use it to style a web page that has been intentionally divided into a number of separate content regions so we can experience how SASS allows us to target our styles more efficiently.

You’ll find all the files related to the worked example in the pbs184-workedExample folder in the instalment ZIP

This folder contains the following files and folders:

  1. index.html — the example web page we’ll be styling. This page Imports its style sheet from ./index.css (which does not exist yet).
  2. index.scss — the SASS file where we’ll be adding our code, initially it contains:
    1. Some useful comments to explain the file’s purpose
    2. An @import CSS at-rule to import the Google Font web fonts we’ll be using for the page

To get started:

  1. Open a terminal in your copy of the pbs184-workedExample folder — so we can compile our SASS to CSS
  2. Open both index.html & index.scss in your favourite code editor — the former for reference, the latter for building our style sheet

The Example HTML

The HTML source code for our example is ready for us to style. To understand the SASS code we’ll be writing it’s important to understand this page’s structure.

In the <head> section it simply defines the usual metadata, imports Font Awesome, and then tries to load the generated CSS from index.css:

<head>
  <meta charset="utf-8" />
  <title>PBS 184 (Worked Example)</title>

  <!-- Import the compiled CSS -->
  <link rel="stylesheet" href="./index.css">
</head>

The body uses the semantic HTML tags to break the page’s content into two top-level chunks:

  1. A <header> tag containing the page title and the instructions for compiling the SASS code wrapped in a <section> tag.
  2. A <main> tag containing two pieces of content each, wrapped in <article> tags.

Both the <section> and <article> tags have meaningful IDs so they can be targeted for styling.

For now, the content of each section doesn’t matter, so here’s the page’s big-picture structure:

<body>
  <header>
    <h1>PBS 184 — Worked Example</h1>

    <section id="instructions">
      <!-- … -->
    </section>
  </header>

  <main>
    <article id="wisdom">
      <!-- … -->
    </article>
    <article id="humor">
      <!-- … -->
    </article>

  </main>
</body>

Getting Started — Compiling the Placeholder SASS File

Before opening the HTML file in a browser, the SASS file needs to be compiled to CSS.

The simplest way to do this is with a one-off SASS compilation, the command for which is simply:

npx sass index.scss index.css

As a quick reminder, the four parts of this command are:

  1. The NodeJS npx command which finds executable files in installed NodeJS modules, both modules installed into the current folder (there are none), and modules installed globally (how we installed sass earlier).
  2. The name of the executable we would like npx to find and then execute for us, in this case, the sass command.
  3. The first argument for the sass command, the path to the SASS code.
  4. The second argument for the sass command, the path we’d like our SASS code compiled to.

If everything’s working as expected, running the command should not generate any output, but it should create a pair of files named index.css and index.css.map. We expected the first of those two files, but what about that second file with the .css.map file extension?

index.css.map is a so-called source-map file. These kinds of files are used by some developer tools to re-map line numbers in generated files back to the matching lines in the original source code files they were generated from. In this case the file allows IDEs that support source maps to connect line numbers in the generated index.css file back to their matching line numbers in the index.scss SASS file it was generated from.

More Efficient Builds with Watch Mode

When developing it can be quite frustrating to keep manually re-compiling your SASS code each time you made a change, so I prefer to use sass’s watch mode to detect changes my SASS files automatically, and then just re-build the CSS for me. The syntax for watch mode is a little different to the regular syntax:

npx sass --watch source_files:output_files

For us that means we need to use the command:

npx sass --watch index.scss:index.css

When you execute that command SASS should report building two files:

$ npx sass --watch index.scss:index.css
[2026-03-29 17:17] Compiled index.scss to index.css.
Sass is watching for changes. Press Ctrl-C to stop.

Notice that the command does not exist after generating the files. It keeps running, and will do so until you intentionally quite the process with crtl+c.

Our Initial Generated CSS

Before we start adding our styles, let’s examine both our starting SASS code and the CSS it compiles to.

Here’s our initial SASS file (index.scss):

/* 
 * This style sheet has been generated from the SASS file ./index.scss.
 * 
 * To re-build this file from the latest version of the SASS file, run one of:
 * - A one-time build   : `sass index.scss index.css`
 * - A watch mode build : `sass --watch index.scss:index.css`
 */

 //
 // === Page Setup — DO NOT EDIT THIS SECTION ===
 //

/*
 * Import Desired Google Fonts:
 * - Ubuntu Sans      : the sans-serif body font from the PBS site
 * - Ubuntu Mono      : the mono-spaced heading font from the PBS  site
 * - Ubuntu Sans Mono : a mono-spaced sans-serif font that compliments the PBS site typography
 * - Indie Flower     : a handwriting font that compliments the PBS site typorgraphy
 */
@import url('https://fonts.googleapis.com/css2?family=Indie+Flower&family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Ubuntu+Sans+Mono:ital,wght@0,400..700;1,400..700&family=Ubuntu+Sans:ital,wght@0,100..800;1,100..800&display=swap');

//
// === END PAGE SETUP ===
//

// Insert the styles from the worked example in the show notes here

Let’s see what that SASS code compiled to in index.css:

/* 
 * This style sheet has been generated from the SASS file ./index.scss.
 * 
 * To re-build this file from the latest version of the SASS file, run one of:
 * - A one-time build   : `sass index.scss index.css`
 * - A watch mode build : `sass --watch index.scss:index.css`
 */
/*
 * Import Desired Google Fonts:
 * - Ubuntu Sans      : the sans-serif body font from the PBS site
 * - Ubuntu Mono      : the mono-spaced heading font from the PBS  site
 * - Ubuntu Sans Mono : a mono-spaced sans-serif font that compliments the PBS site typography
 * - Indie Flower     : a handwriting font that compliments the PBS site typorgraphy
 */
@import url("https://fonts.googleapis.com/css2?family=Indie+Flower&family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Ubuntu+Sans+Mono:ital,wght@0,400..700;1,400..700&family=Ubuntu+Sans:ital,wght@0,100..800;1,100..800&display=swap");

/*# sourceMappingURL=index.css.map */

Notice the SASS comments didn’t get mapped through, but CSS comments did. Also notice that the CSS at-rule for importing Google Fonts (@import …) was not executed by the SASS compiler, but simply passed through. This is because this is a CSS at-rule not an SCSS at-rule.

Finally, notice the specially formatted comment at the bottom of the file referencing the source map file — it’s this comment’s presence that allows IDE with source map support to connect the generated source map file to this specific file.

Now that we have our generated CSS, open index.html in your favourite browser to see the initial HTML page with without any styles applied.

Testing Watch Mode

To prove that the watch-mode build is working, let’s add a test style to the playground area of the SASS file:

// TEST CODE - DO NOT KEEP
section#instructions {
    color: silver;
    font-size: 0.5rem;
}

Save the file, and you should see the running sass command detect the file change and re-build the files:

>>> Change detected to: index.scss
      write index.css
      write index.css.map

Refresh the page in your browser and you should see the instruction text at the top of the file shrink to an unusably small size.

This is obviously not a helpful change, so remove the temporary code from index.scss and save the file again.

Add Initial SASS Variables

In real-world projects you always know there are some things you want to capture in global variables so you can re-use and tweak them throughout your code, but you won’t know all the things you’d like to customise.

Since this is a worked example, let’s start by capturing the obvious things for this page, and we’ll continue add more variables as we go.

Replace the // Insert the styles from the worked example in the show notes here placeholder in index.scss with the following code:

//
// === Define Global Variables ===
//

// Typograhy Varialbes - capture the PBS house style
$body-font-family: 'Ubuntu Sans', sans-serif;
$body-font-weight: normal;
$body-line-height: 1.5; // replicate Bootstrap convention
$heading-font-family: 'Ubuntu Mono', monospace;
$heading-font-weight: bold;
$code-font-family: 'Ubuntu Sans Mono', monospace;
$handwriting-font-family: 'Indie Flower', cursive;
// Add additional globoal typography-related variables here as needed

// Colour Variables - capture the PBS house style
$logo-colour: #00408d;
$secondary-color: #5f5f5b;
$body-background-color: white;
$body-text-color: black;
$heading-text-color: $logo-colour;
$secondary-heading-text-color: $secondary-color;
$code-text-color: $secondary-color;
// add additional global colour-related variables here as needed

I want to draw your attention to a few things:

  1. I intentionally used SASS comments for this entire section because none of these variables will appear in the generated CSS, so commends describing them will be meaningless in that context.
  2. I re-used the value of the $logo-color and $secondary-color variables in later variable definitions. The order is important, you can’t use variables before you assign them values!
  3. These variables have all been defined at the top-level of SASS code (not inside curly braces), so they are all global variables, and can be used anywhere in our code.

Save the file, allow SASS to re-build the CSS file, then refresh your browser.

You’ll notice that literally noting has changed, because none of these variables have been used to produce any style declarations yet!

Configure the Overall Spacing and Style for the Page

Note: for convenience you’ll find a copy of the full index.scss code as it should be at this point in the example in index-1-initialVars.scss.

Now that we have our variables defined, let’s make use of them by addressing as much as we can at the top-level of the page.

At the body level we need to set:

  1. The basic colours, specifically the page’s background and the default text colour
  2. The default typography, specifically the font family, size, weight, and line spacing
  3. The box model for the body tag

The first thing we want to do is control the body’s spacing around the edge of the window, the body’s background colour, and the very top-level typography defaults.

Let’s start by appending the following to index.scss:

/*
 * === Basic page setup ===
 */

/* Default colours, typography and body spacing */
body {
  background-color: $body-background-color;
  color: $body-text-color;
  font-family: $body-font-family;
  font-size: 1rem; /* accept the browser's configured default font-size  */
  font-weight: $body-font-weight;
  line-height: $body-line-height;
  margin: 0px; /* follow Bootstrap's approach and set margins at the block level */
  padding: 0px; /* follow Bootstrap's approach and set margins on containers rather than the body */
}

I want to draw your attention to two things:

  1. I chose to follow Bootstrap’s approach to the default font size and not override the browser’s own default pixel size, but instead, explicitly accept it by setting the size to 1rem, i.e. to the root-level default font size.
  2. I also choose to adopt Bootstrap’s guidance for handling spacing (margins and paddings). It’s easier to control the page as a whole when the <body> tag has no spacing.

Save the file, let it build, and you’ll see that the above SASS compiles down to the following CSS:

/*
 * === Basic page setup ===
 */
/* Default colours, typography and body spacing */
body {
  background-color: white;
  color: black;
  font-family: "Ubuntu Sans", sans-serif;
  font-size: 1rem; /* accept the browser's configured default font-size  */
  font-weight: normal;
  line-height: 1.5;
  margin: 0px; /* follow Bootstrap's approach and set margins at the block level */
  padding: 0px; /* follow Bootstrap's approach and set margins on containers rather than the body */
}

Not much to note here, other than that the SASS variables have all been compiled down to explicit CSS values.

Refreshing the browser shows we now have the standard PBS body typography, but the spacing around our content is all messed up.

Let’s make a start on addressing that. We have two top-level container tags in our markup — the <header> tag contains the page header and the instructions, and the <main> tag contains the rest of the content. We need to add some margins top-and-bottom to both separate the containers from each other, and, to separate them from the top and bottom of the page’s body. Again, rathe than re-inventing the wheel I’ve simply adopted Bootstap’s approach. Append the following to the bottom of index.scss and allow the stylesheet to rebuild:

/* Configure the spacing for the top-level containers (header & main) */
header, main {
  /* replicate Bootstrap's approach for containers */
  margin: 1.5rem 0px; 
  padding: 0px 1.5rem;
}

This is all pure CSS, so the compiler will pass it through un-changed.

Refresh the page again, and you’ll see our content is now better spaced — it still doesn’t look quite right, but it’s not touching the edges anymore!

At this point we’ve correctly configured the body-level spacing and the container-level spacing, so the next step is the standard block tags inside our various containers — in other words, headings and paragraphs.

Since we’ve been following Bootstrap’s spacing conventions for the previous two levels, it only makes sense to do so again. For block level elements Bootstrap adopts the following approach:

  1. Add all margins in a single direction, below
    1. Default to 1rem for all block-level tags
    2. Make an exception for headings, and set those to 0.5rem
  2. Zero out the padding by default, then make exceptions as needed for special tags

For now, we’re only concerned with headings and paragraphs, but we know we’ll need to apply this same approach to other tags like block quotes and definition lists later in the process, so this is the perfect opportunity to define a simple mixin.

We need our mixin to always remove the padding, and to remove all margin but the bottom, and then to set it to 0.5rem for headings, and 1rem for everything else. It makes sense to implement the needed exception with a single, optional, boolean argument to enable the exception for headings, and default it to false.

Append the following mixin code to index.scss:

// mixin to replicate the Bootstrap spacing convention for block-level elements
// Note: margins and paddings are *not* inherited, so they do not cascade from the body.
// - $header: if true, use the Bootstrap convention for headers, else, for paragraphs
@mixin block-spacing($header :false) {
  // set all padding to 0px
  padding: 0px;

  // always set all margins but the bottom to 0px
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;

  // use a small bottom margin for headers and a bigger one for everything else
  @if $header {
    margin-bottom: 0.5rem;
  } @else {
    margin-bottom: 1rem;
  }
}

Since mixins exist purely within SASS, adding this mixin won’t change the generated CSS.

Now that we have our mixin, let’s use it to configure our headers and paragraphs.

We need these styles to do more than just the spacing, so as well as using our new mixin, we also need to add some style declarations to:

  1. Make lead paragraphs stand out a little by making them a little bigger
  2. Make lead <span> tags within paragraphs stand out by making them bold.
  3. Set the typography for our headings

As a first pass, the following SASS code should work:

/* Default Paragraph Styles */
p { 
  @include block-spacing;

  span.lead { /* make lead spans within paragraphs stand out */
    font-weight: bold; 
  }
}
/* Make lead paragraphs stand out */
p.lead {
  font-size: 1.25rem;
}

/* Default Heading Styles */
h1, h2, h3, h4, h5, h6 {
  font-family: $heading-font-family;
  font-weight: $heading-font-weight;
  color: $heading-text-color;
  @include block-spacing(true); // pass the optoinal argument to use header spacing
}

There are two things of note here:

  1. The use of selector nesting to make only <span> tags with the class .lead inside <p> tags bold
  2. The two different uses of our mixin — once without arguments, and once with the one optional arguments:
    1. For paragraphs completely omitted the parentheses since we had no argus to pass: @include block-spacing;)
    2. For the headings we needed to pass true as the first argument, so we did need to add parentheses: @include block-spacing(true);

However, this code is missing something — we’re accepting the browser defaults for the heading font sizes. We really should be explicit about that!

This is the perfect opportunity to use a SASS map and a loop!

Rather than re-inventing the wheel I’m replicating Bootstrap’s approach yet again, so add the following SASS map directly after $heading-font-weight: bold; in the variable definitions near the top of the index.scss:

$heading-font-size-map: ( // replicate Bootstrap's sizes
  h1: 2.5rem,
  h2: 2rem,
  h3: 1.75rem,
  h4: 1.5rem,
  h5: 1.25rem,
  h6: 1rem
);

We can now replace our single style declaration for all six headers with a loop to produce six different style declarations:

/* Default Heading Styles */
@each $heading, $size in $heading-font-size-map {
  #{$heading} {
    font-family: $heading-font-family;
    font-weight: $heading-font-weight;
    color: $heading-text-color;
    font-size: $size;
    @include block-spacing(true); // pass the optoinal argument to use header spacing
  }
}

Notice the use of the interpolation operator (#{$heading}) to use the keys from the map as the selectors for the style declarations.

Putting it all together the final code for the default styling of paragraphs and headers is now:

// mixin to replicate the Bootstrap spacing convention for block-level elements
// Note: margins and paddings are *not* inherited, so they do not cascade from the body.
// - $header: if true, use the Bootstrap convention for headers, else, for paragraphs
@mixin block-spacing($header :false) {
  // set all padding to 0px
  padding: 0px;

  // always set all margins but the bottom to 0px
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;

  // use a small bottom margin for headers and a bigger one for everything else
  @if $header {
    margin-bottom: 0.5rem;
  } @else {
    margin-bottom: 1rem;
  }
}

/* Default Paragraph Styles */
p { 
  @include block-spacing;

  span.lead { /* make lead spans within paragraphs stand out */
    font-weight: bold; 
  }
}
/* Make lead paragraphs stand out */
p.lead {
  font-size: 1.25rem;
}

/* Default Heading Styles */
@each $heading, $size in $heading-font-size-map {
  #{$heading} {
    font-family: $heading-font-family;
    font-weight: $heading-font-weight;
    color: $heading-text-color;
    font-size: $size;
    @include block-spacing(true); // pass the optoinal argument to use header spacing
  }
}

Make sure your file has the latest version pasted in, save, and allow the style to re-build. The following CSS will be produced:

/* Default Paragraph Styles */
p {
  padding: 0px;
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 1rem;
}
p span.lead { /* make lead spans within paragraphs stand out */
  font-weight: bold;
}

/* Make lead paragraphs stand out */
p.lead {
  font-size: 1.25rem;
}

/* Default Heading Styles */
h1 {
  font-family: "Ubuntu Mono", monospace;
  font-weight: bold;
  color: #00408d;
  font-size: 2.5rem;
  padding: 0px;
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 0.5rem;
}

h2 {
  font-family: "Ubuntu Mono", monospace;
  font-weight: bold;
  color: #00408d;
  font-size: 2rem;
  padding: 0px;
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 0.5rem;
}

/* … */

h6 {
  font-family: "Ubuntu Mono", monospace;
  font-weight: bold;
  color: #00408d;
  font-size: 1rem;
  padding: 0px;
  margin-top: 0px;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 0.5rem;
}

I want to draw your attention to a few things:

  1. Notice how the SASS compiler split out the nested selector for lead spans within paragraphs into its own style definition.
  2. Notice I chose to truncate the styles generated for the headers. Why? Because the SASS loop generated six very similar declarations, only the font-size differs between all six.
  3. Notice that the spacing styles generated by the block-spacing mixin appear a total of seven times in the final CSS — that’s a lot of value from the small amount of extra work it too to build the mixin!

Once the style has built, refresh the browser and you’ll see we now have good default headings and paragraphs.

That gets the overall page into a good state, so let’s now focus in on each of the content regions, starting with the header.

Style the Header Region

Note: for convenience you’ll find a copy of the full index.scss code as it should be at this point in the example in index-2-globalStyles.scss.

Visually, the header section has two different parts — it has a page-level title, and it has a note explaining what the page is and how to build the CSS. This distinction is also reflected in the markup, which has the following basic structure:

<header>
  <h1>PBS 184 — Worked Example</h1>
  <section id="instructions"></section>
</header>

Let’s start by styling the heading inside the header to make it look more like a Bootstrap display header. In practical terms that means we want to:

  1. Use a simple SANS font
  2. Make it very thin (small font weight)
  3. Make it very big
  4. Make it an appropriately different colour to regular headings

Let’s start by capturing our values for those properties in new global variables. Add the following directly above the shown placeholder lines near the top of index.scss:

$banner-font-family: $body-font-family;
$banner-font-weight: 100;
$banner-font-size: 5rem;
// Add additional globoal typography-related variables here as needed

And add the following a little lower down the file directly above the placeholder line for colour-related variables ():

$banner-text-color: $secondary-heading-text-color;
// add additional global colour-related variables here as needed

Now that we have our variables defined, we can use them to make targeted style that will only affect <h1> tags within the <header> container tag. Append the following to the bottom of index.scss

/*
 * === Customise the Specific Page Regions ===
 */

/* Style the header region */
header {
  /* Style the page-level header without affecting other headers */
  h1 {
    font-family: $banner-font-family;
    font-weight: $banner-font-weight;
    color: $banner-text-color;
    font-size: $banner-font-size;
  }
  
  /* TO DO — style the instructions section */
}

Next, let’s tackle the instructions section. We need to make it clear that this is very different content to the rest of the page, there are lots of ways we could do this, but I’ve chosen the following approach:

  1. Add a border and some appropriate extra spacing
  2. Add a subtle background colour
  3. Lighten the text in some way to make is obviously different to the body text
  4. Style the code snippets so they use an appropriate font, and so they stand out appropraitely

Update the header declaration in index.scss to match this updated version:

/*
 * === Customise the Specific Page Regions ===
 */

/* Style the header region */
header {
  /* Style the page-level header without affecting other headers */
  h1 {
    font-family: $banner-font-family;
    font-weight: $banner-font-weight;
    color: $banner-text-color;
    font-size: $banner-font-size;
  }

  /* Style the instrunctions */
  section {
    /* clearly enclose the instructions */
    border: 1px solid $logo-colour;
    border-radius: 0.5em;
    padding: 1em;
    background-color: color-mix(in srgb, $logo-colour, white 90%);

    /* clear space around the instructions */
    margin-bottom: 2rem;

    /* make the text more subtle */
    color: $logo-colour;
    font-size: 0.8rem;

    /* compress the paragraphs a little */
    p {
      margin-bottom: 0.8em;
    }

    /* help the code snippets stand out */
    code {
      color: $code-text-color;
      font-family: $code-font-family;
    }
    pre {
      color: $code-text-color;
      font-family: $code-font-family;
      border-left: 3px solid $code-text-color;
      padding-left: 1em;
      margin-bottom: 0.8em;
    }
  }
}

Notice the extensive use of nesting to clearly define the targets of all these style declarations. Also notice the heavy use of variables.

Save the file and let the CSS rebuild. This SASS file will convert to the following CSS:

/*
 * === Customise the Specific Page Regions ===
 */
/* Style the header region */
header {
  /* Style the page-level header without affecting other headers */
}
header h1 {
  font-family: "Ubuntu Sans", sans-serif;
  font-weight: 100;
  color: #5f5f5b;
  font-size: 5rem;
}
header {
  /* Style the instrunctions */
}
header section {
  /* clearly enclose the instructions */
  border: 1px solid #00408d;
  border-radius: 0.5em;
  padding: 1em;
  background-color: color-mix(in srgb, #00408d, white 90%);
  /* clear space around the instructions */
  margin-bottom: 2rem;
  /* make the text more subtle */
  color: #00408d;
  font-size: 0.8rem;
  /* compress the paragraphs a little */
}
header section p {
  margin-bottom: 0.8em;
}
header section {
  /* help the code snippets stand out */
}
header section code {
  color: #5f5f5b;
  font-family: "Ubuntu Sans Mono", monospace;
}
header section pre {
  color: #5f5f5b;
  font-family: "Ubuntu Sans Mono", monospace;
  border-left: 3px solid #5f5f5b;
  padding-left: 1em;
  margin-bottom: 0.8em;
}

I want to draw your attention to a few things:

  1. Notice how the SASS compiler has split the nested style declarations in our source code into separate style declarations with their valid CSS containment selectors like header section pre
  2. Notice that because I chose to use the CSS function color-mix() to do the colour math the SASS compiler passed it through rather than executing. This demonstrates the difference between SASS functions and CSS functions nicely — the compiler will execute the SASS functions, but it will pass the CSS function calls through un-changed so the browser can execute them when displaying the page.

SASS provides a module for colour math if you prefer to have the math happen at compile time rather than in the browser. The relevant function are contained in the sass:color modules which is documented here.

Customise the ‘Wisdom’ Article

Note: for convenience you’ll find a copy of the full index.scss code as it should be at this point in the example in index-3-headerStyles.scss.

The only thing that makes this article different to the rest of the page is that it uses block quotes, and we’ve not defined a base style for <blockquote> tags yet. Before we add any additional flare appropriate to the specific quotations in this section we should define some baseline styles for all block quotations.

We need to ensure block quotes:

  1. Apply the appropriate spacing:
    1. Use the standard block-level spacing from the block-spacing mixin as a starting point
    2. Add padding around the quotation so it has room to breath and looks noticeably different to a regular paragraph
  2. Emphasis the quotation by increasing the font size

Insert the following code into index.scss directly after the @each loop for the headings:

/* Default block quote styles */
blockquote {
  // start by applying the standard block-level spacing
  @include block-spacing;
  padding: 2rem; /* increase the padding */
  font-size: 1.5rem; /* increase the font size */
}

Save the file and allow the style to re-build. Refresh the browser and you’ll see the quotations now stand out from regular paragraphs appropriately.

This is a good default style and could be used for any quotation from any source, but we want to do a little more with the wise quotes in this article.

What we want to do is humanise these wise quotations by:

  1. Using a large cursive colourful font for the quotation text
  2. De-emphasise the author details so there’s a clear importance hierarchy:
    1. The words are the most important, so they need to catch the eye most
    2. The author is less important but still important, so it should catch the eye second
    3. The description of the author is the least important, so it should catch the eye least of the three

Add the following to the bottom of index.scss:

/* Style the wise quotations */
article#wisdom {
  blockquote {
    font-family : $handwriting-font-family; /* Use a cursive font */
    color: rgb(from $logo-colour r b g); /* use the CSS function rgb() to swap the blue and green channels of the logo colour */
    font-size: 3rem; /* increase the size of the quotations */

    p { /* collapse the vertical space between the quotation and the author */
      margin-bottom: 0px; 
    }

    p.author { /* override the font for the author of the wise quotation */
      font-family: $body-font-family;
      color: $body-text-color;
      padding-left: 3rem;
      text-transform: uppercase;
      font-size: 1.25rem;

      span.description { /* override the font for the author descriptions */
        color: $secondary-heading-text-color;
        text-transform: none;
        font-size: 1rem;
      }
    }
  }
}

There are quite a few things to note here:

  1. To get a pleasing colour without hard-coding it I chose to use the rgb() CSS function to split the logo colour into it’s red, green, and blue components, and then re-combine them with their blue and green components swapped, this gives a shade of green that matches the heading colour used for the headings nicely
  2. This is our most complex of the power of selector nesting — we are using ever more deeply nested scopes to repeatedly override certain style properties:
    1. The global style for all block quotes we defined previously set the font size to 1.5rem, with our nesting we override that for all text in block quotes within our article and set it to 3rem, then for author paragraphs within block quotes within our article we override it again to 1.25rem, and finally for description spans inside author paragraphs inside block quotes in our article we override it one last time to 1rem!
    2. The colour is similarly set for all block quotes in our article, then overridden for author paragraphs within those block quotes, and again for description spans within those author paragraphs
    3. Finally the there is a text transform applied to convert the entire author paragraph to upper case, and then that is overridden for description spans inside author paragraphs to remove the transformation.

Save the file, allow the CSS to rebuild and refresh your browser. Hopefully you agree our quotations now have plenty of personality, and the words dominate over the authors who dominate over their descriptions.

Customise List of Humorous Quotes

_Note: for convenience you’ll find a copy of the full index.scss code as it should be at this point in the example in index-4-wisdom.scss.

What makes this section different is that it uses a definition list, and we’ve not defined any global styles for those yet.

Definition lists are block-level elements, so for the top level they should be spaced like other block level elements, that is to say, using our block-spacing mixin.

Definition lists contain more blocks, definition titles and definition data. These blocks need to be spaced too, and the most sensible thing to do is to treat the definition titles like headings, and the definition data like regular blocks.

Add the following to index.scss directly after the global definition we added for the block quotes in the previous section:

/* Default definition list styles */
dl {
  /* apply the default block-level spacing */
  @include block-spacing;

  dt {
    /* apply the default block-level spacing for headers*/
    @include block-spacing(true); // pass the optoinal argument to use header spacing

    /* Emphasise the title text */
    font-weight: bold;
  }
  dd {
    /* apply the default block-level spacing */
    @include block-spacing;

    /* indent the left-side */
    padding-left: 2rem;
  }
}

Nothing new here, but worth drawing attention to the fact that we continue to get value from our block-spacing mixin as we work our way through this project.

Like with the wise Quotations, we want to add a little personality to the quotations, and we should do so in a similar way, but with appropriate changes for the list format. This should achieve our goals:

  1. Use a cursive font for the quotations, and make it bigger than regular text, but not quite as big as the wise quotations.
  2. Use the same typographic approach as before for the names — a little bigger than regular text, all upper case, but not bold.

Append the following to the end of index.scss:

/* Style the humourous quotations */
article#humour {
  dl {
    dt {
      font-size: 1.25rem; /* increase the size of the comedian names */
      text-transform: uppercase;
      font-weight: normal; /* override the default bolding of dt elements */
    }
    dd {
      font-family: $handwriting-font-family; /* Use a cursive font */
      font-size: 2rem; /* increase the size */;
      color: rgb(from $logo-colour r b g); /* use the CSS function rgb() to swap the blue and green channels of the logo colour*/
    }
  }
}

For the most part this is very similar to what we did for the wise quotation article — heavy use of selector nesting to target or humanising tweaks to just definition lists in this article.

However, there is a bad smell in this code — we have copied-and-pasted the complex CSS colour function calculation for the cursive writing between the wise quotations style definition and this style definition. This means that if we change our minds we need to remember to fix it twice. This is the perfect opportunity to illustrate a powerful little subtlety in how SASS handles CSS function calls.

Remember, SASS can’t actually call CSS functions, but that does not mean we can’t store the CSS function call in a SASS variable! From SASS’s point of view, a CSS function call is just a piece of valid CSS data, so it will store and then re-produce the call just like it would a colour or a font family or a dimension! But it’s actually a little cleverer than that, it won’t just store it as a string, it will replace SASS variable names with their values at compile time, so what appears in the output CSS is a version of the same function call with an actual value where the SASS variable was.

Let’s fix our bad smell by adding a new global colour variable directly above the appropriate placeholder near the top of index.scss:

$handwriting-text-color: rgb(from $logo-colour r b g); /* use the CSS function rgb() to swap the blue and green channels of the logo colour */
// add additional global colour-related variables here as needed

We can no do a find-and-replace for color: rgb(from $logo-colour r b g); /* use the CSS function rgb() to swap the blue and green channels of the logo colour */ and replace it with simply color: $handwriting-text-color;!

Save the file, allow the CSS to generate. Inspecting the generated CSS for the relevant selectors for our handwriting colour shows that SASS did intelligently pass the function call through, retaining its structure but substituting in the actual value of the $handwriting-text-color SASS variable:

article#wisdom blockquote {
  font-family: "Indie Flower", cursive; /* Use a cursive font */
  color: rgb(from #00408d r b g);
  font-size: 3rem; /* increase the size of the quotations */
}
/* … */
article#humour dl dd {
  font-family: "Indie Flower", cursive; /* Use a cursive font */
  font-size: 2rem; /* increase the size */
  color: rgb(from #00408d r b g);
}

Once the CSS has generated, refresh the page, and hopefully you agree that we now have a nicely styled page.

You’ll find the full final version of the code in the file index-5-final.scss.

Final Thoughts

This has been a whistle-stop tour of SCSS. Hopefully you’ve picked up enough to start using this powerful technology in your own projects, and, to be able to find what ever details you need in the documentation.

In the next instalment we’re going to see how Bootstrap facilitates customisation via SASS by provoding a collection of SASS variables and maps to override and extend, and SASS mixins to use in our own SASS code.

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack