14  Sass

Stuff I’ve learned to do with Sass (which usually means I’m writing SCSS). My relationship status with Sass is described in my ever-evolving docspo post.

14.1 How to compile a Sass file to a CSS one at the command line

Sometimes you just want to see what your SCSS will look like as CSS. If you have Dart Sass installed (if not, and you’re on a Mac, I recommend brew install sass), you can run the following from the command line:

Terminal
sass style.scss:style.css

14.2 How to use & (the Sass ampersand)

Using the Sass & selector is actually quite easy, but it’s mildly annoying to look up (given it’s an ampersand). So, I’m stashing some notes and resources on it here.

In the official Sass documentation, & is called the parent selector, “a special selector invented by Sass that’s used in nested selectors to refer to the outer selector(The Sass Team 2023).

If you’re new to the notion of nesting, Richard Finelli’s The Sass Ampersand (2017) gives a handy, quick overview. You use & to nest when you want your selection to have both the parent (surrounding) class and the inner child class. To use one of Finelli’s example, the following two SCSS statements are equivalent:

SCSS
// basic nesting
.parent {
  .child {}
}

// nesting with &
.parent {
  & .child {}
}

The above statements translate to the same CSS:

CSS
.parent .child {}

Using & with pseudo classes

My primary use case for & is with pseudo classes, primarily those related to links and buttons (e.g. :hover, or :focus). For example, I use the following code to style links that are direct children of the .citation class and add additional properties and values when they are hovered or focused:

SCSS
.citation>a {

  &:hover {}

  &:focus-visible {}
  
}

The second & statement refers to its parent (.citation>a) and is not a descendant of the previous one. So, my generated CSS will have styles for the following:

CSS
.citation>a {}

.citation>a:hover {}

.citation>a:focus-visible {}

I find it makes for a nice way to organize your code, especially when you’re using transitions.

Learn more

In addition to the official Sass docs, I found the following posts helpful:

14.3 How to include one class in another (@extend)

Sometimes you have an element that you want to style with the properties of another class and give it some more properties of its own. Both Quarto and Bootstrap make heavy use of something called the BEM (Block, Element, Modifier) methodology to do this (for example, inspect the HTML for Quarto’s callout blocks), which involves a lot of markup where you have to apply parent and modifier classes to an element.

There are a couple of ways to do this with Sass, the simplest of which is to use @extend. How does it work? As the Sass docs describe:

@extend updates style rules that contain the extended selector so that they contain the extending selector as well.

So, if an element is styled with the extender, it’s also styled as if it matches the class that’s being extended. This means you don’t need to apply both classes to an element in order for the parent-class properties to be applied (as you can see in the CSS generated from the SCSS, below).

.myclass {
    font-weight: bold;
    font-size: 90px;
}

.myotherclass {
    @extend .myclass;
    color: #000000;
}
.myclass, .myotherclass {
  font-weight: bold;
  font-size: 90px;
}

.myotherclass {
  color: #000000;
}

Adapted from F21’s response to Including another class in SCSS.

Extends vs mixins

Like extends, Sass mixins (@mixin) let you define styles for re-use in your style sheet. Mixins are a bit more advanced than extends, since they can take arguments, allowing you to configure the styles. In the event that you don’t need such configuration, they still work a bit differently. With mixins you declare a group of declarations that are reused when invoked with @include. The styles are, in effect, copied to the current rule, but there is none of the “intelligent unification” that you get with extends—you’re effectively just copying a chunk of styles.

Per the Sass docs, Extends or mixins:

As a rule of thumb, extends are the best option when you’re expressing a relationship between semantic classes (or other semantic selectors).

The way the CSS is compiled also differs significantly. For a solid explanation with greater detail, see When to use @extend; when to use a mixin by Harry Roberts.

14.4 How to set a text color based on the background color

The goal here was to decide if I wanted the foreground color (i.e. the color of the text) to be black or white, based on the background color. I wanted to do this in order to recreate a chart of color shades similar to that used in the Bootstrap-colors documentation.

My initial approach was just to manually set the color property manually for each shade, e.g.:

.pink-400 {
  background-color: $pink-400;
  color: black;
}
.pink-900 {
  background-color: $pink-900;
  color: white;
}
.pink-400 {
    background: #ff7893;
    color: #000;
}
.pink-900 {
    background: #331118;
    color: #fff;
}

However, you can actually do this with a Sass @function!

In Kevin Powell‘s Dynamic text color with Sass (2018), he takes the following approach. Write a function, text-clr() that takes a color as an argument, and then use Sass’ built-in lightness($color) function in an @if/@else rule block to decide whether or not the text color should be white or black.

Powell describes his code (which follows):

“…what I’ve told it to do is spit out black if my lightness is greater than 50, and spit out white if my lightness is less than 50.”

SCSS
$bg: #000; // my background color

@function text-clr($color) {
  @if (lightness($color) > 50) {
    @return #000;
  } @else {
    @return #fff;
  }
}

To actually use this function, you’d set a background color ($bg), and then apply it like so:

SCSS
body {
  background: $bg;
  color: text-clr($bg);
}

Powell then goes on to use one of my favorite Sass features, @mixins, to make things even easier (i.e. avoid having to write out the same colour twice).

SCSS
@mixin dc($color) {
  // dc is short for dynamic color
  background: $color;
  color: text-clr($color);
}

Now you can achieve the same rendered CSS as before by using the dc() mixin with @include:

SCSS
body {
  @include dc($bg);
}

So, with the function and mixin added, the same code I shared at the beginning now looks like this:

.pink-400 {
  @include dc($pink-400);
}
.pink-900 {
  @include dc($pink-900);
}
.pink-400 {
    background: #ff7893;
    color: #000;
}
.pink-900 {
    background: #331118;
    color: #fff;
}

A more perceptually robust version can be made using luminance in place of lightness—see, e.g. Using Sass to automatically pick text colors (Gomes 2016); or Programming Sass to Create Accessible Color Combinations (Hogue 2020).

Note

In the Bootstrap 5.2.0 beta release they announced the addition of .text-bg-{color} helpers to achieve this same thing: choose a foreground color based on the contrast with a chosen background color. You can see their implementation in _color-bg.scss.

14.5 How to loop through a Sass color map

I wanted to create color swatches showing contrast ratios in the same way done in the Bootstrap 5.1 docs on colors. In order to do so, you use a color Sass map, like those included in Bootstrap’s variables, e.g. the built-in $colors Sass map is:

SCSS
$colors: (
  "blue":       $blue,
  "indigo":     $indigo,
  "purple":     $purple,
  "pink":       $pink,
  "red":        $red,
  "orange":     $orange,
  "yellow":     $yellow,
  "green":      $green,
  "teal":       $teal,
  "cyan":       $cyan,
  "white":      $white,
  "gray":       $gray-600,
  "gray-dark":  $gray-800
);

From the Sass-lang docs on Maps (Lintorn-Catlin et al. 2021):

Maps in Sass hold pairs of keys and values, and make it easy to look up a value by its corresponding key. They’re written (<expression>: <expression>, <expression>: <expression>). The expression before the : is the key, and the expression after is the value associated with that key.

With Bootstrap, you can modify color maps using map-merge(). You do something for every item in a pair using the the Sass @each rule (Lintorn-Catlin et al. 2021).

The @each rule makes it easy to emit styles or evaluate code for each element of a list or each pair in a map. It’s great for repetitive styles that only have a few variations between them. It’s usually written @each <variable> in <expression> { ... }, where the expression returns a list.

You can expand the code below to see the loop used to generate color swatches for each item in the $colors map (originally from the Bootstrap docs source code).

Show the code
SCSS
//
// Docs color palette classes
//

@each $color, $value in map-merge($colors, ("gray-500": $gray-500)) {
  .swatch-#{$color} {
    color: color-contrast($value);
    background-color: #{$value};

    &::after {
      $contrast-ratio: "#{contrast-ratio($value, color-contrast($value))}";
      $against-white: "#{contrast-ratio($value, $white)}";
      $against-black: "#{contrast-ratio($value, $black)}";
      position: absolute;
      top: 1rem;
      right: 1rem;
      padding-left: 1rem;
      font-size: .75rem;
      line-height: 1.35;
      white-space: pre;
      content:
        str-slice($contrast-ratio, 1, 4) "\A"
        str-slice($against-white, 1, 4) "\A"
        str-slice($against-black, 1, 4);
      background-color: $value;
      background-image:
        linear-gradient(
          to bottom,
          transparent .25rem,
          color-contrast($value) .25rem .75rem,
          transparent .75rem 1.25rem,
          $white 1.25rem 1.75rem,
          transparent 1.75rem 2.25rem,
          $black 2.25rem 2.75rem,
          transparent 2.75rem
        );
      background-repeat: no-repeat;
      background-size: .5rem 100%;
    }
  }
}

Using the code above with a custom-color map (with the color-contrast() function built into Bootstrap 5.1), I was able to generate swatches showing the contrast for the background color against the current color (i.e. the text/foreground color), against white, and against black.

Screenshot showing color swatches with three contrast ratio values. Contrast shown for colors named: body-color, red-a11y, teal, blue-a11y, pink, yellow, blue-pale, green-pale, and pink-pale

You can see the rendered color swatches here.