Modifying Prism.js to use a variable font

When researching how best to use Recursive, I did the following exercise. I took the basic selectors for Prism.js and modified them to use Recursive as the primary font.

I did it for the following reasons:

  • We’re already using Recursive throughout the document so we don’t need to download another font
  • Recursive has a monospaced axis that works well for code blocks
  • If needed we can do further work using other features of the variable font

The modified code loooks like this:

code[class*="language-"],
pre[class*="language-"] {
  color: #657b83; /* base00 */
  /* add Recursive to the font stack */
  font-family: Recursive, Consolas, Monaco,
    'Andale Mono', 'Ubuntu Mono', monospace;
  font-size: 1em;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
  word-wrap: normal;

  /* To handle variable font */
  font-weight: 400;
  /* All other axes to their default values */
  font-variation-settings:  "MONO" 1,
                            "CASL" 0,
                            "slnt" 0,
                            "ital" 0.5;

  line-height: 1.5;

  /* Change tab size to 2 */
  -moz-tab-size: 2;
  -o-tab-size: 2;
  tab-size: 2;

  -webkit-hyphens: none;
  -moz-hyphens: none;
  -ms-hyphens: none;
  hyphens: none;
}

There are two minor modifications I made to use Recursive are as follows:

  • Put Recursive as the first font in the font-family stack
  • Add the variable font attributes: font-weight and font-variation-settings

Yes, this tweak requires adding another font to the site but, as mentioned before, I’m already using the font for copy and it’s the same variable font that works for monospaced content so I’m OK with the tradeoff.

Recursive variable font… How To Use it

Recursive is a variable font under development by Arrowtype that caught my attention by the possibilities it provides.

Recursive provides two custom axes: One that moves from monospaced to sans-serif and another one that ranges from standard/linear to more casual/playful styles.

It also provides three standard axes: Weight, Slant, and Italics.

The combination of these 5 axes in 64 pre-defined instance variables makes for a very expressive font that can serve many duties in a site or application without adding font files and impacting performance.

In a previous post I looked at using Recursive in the context of Material Design typography and how to add features that are specific to a font rather than to the design system created with Material Design.

But what would it take to use the font outside a Material Design environment?

The first thing to do is to download the font from Github release page. This will give you the WOFF2 file.

I will use Wakamaifondue to generate the CSS that we’ll use throughout the document. Load the font (currently Beta 25) on Wakamaifondue and save the generated CSS file to your computer. This generated file contains all the classes to enable OpenType features and font named instances.

One thing the font doesn’t have is the @font-face at-rule to actually load the font. The code to actually load the font looks like this.

@font-face {
  font-family: 'Recursive';
  src:
    url('../fonts/recursive.woff2') format('woff2-variations');
  font-weight: 300 1000;
  font-display: swap;
}

There are a few differences to account for the variable font. Instead of woff2, we use woff2-variations to indicate that it’s a variable font compressed using WOFF2.

The font-weight attribute for variable fonts takes two values, representing the minimum and maximum values for the weight. It goes without saying that these values should match those available to the font.

Now we’re ready to start exploring the font.

OpenType features and Predefined Instances

What makes Wakamaifondue so attractive is that it does most of the work for you. It creates variables for each OpenType feature the font makes available and for each predefined instance.

You can combine these classes to get the effects that you want. In the example below, we have a monospaced font and the Slashed Zero OpenType feature.

<pre class="recursive-b025-zero
            recursive-b025-mono-linear">
  All the text in this box is monospaced.

  All the 0 have a diagonal dash on them
  to distinguish them from O
</pre>

All the work Wakamaifondue did upfront means that developers and content authors can use existing structures adding classes to existing elements or wrapping content in the semantically neutral div and span elements to accomplish our objectives.

Using variables

Because Recursive uses custom axes, two of the default axes may conflict with each other and there is an issue with inheritance for font-variation-settings as documented in Boiling eggs and fixing the variable font inheritance problem we would have to use CSS variables or their Houdini equivalents to control individual axes and then merge them together. You can get more information about how to write CSS using Variable Fonts in Variable Fonts: What web authors need to know issue from Jason Pamental’s Responsive Typography Newsletter.

The first block of CSS sets up the default values for each axis and then uses font-weight and font-variation-settings to configure the default values for the font.

:root {
  --vf-mono: 0;
  --vf-casl: 0;
  --vf-wght: 400;
  --vf-slnt: 0;
  --vf-ital: 0.5;
  font-weight: var(--vf-wght);
  /*
    This will define the values for
    the entire document
  */
  font-variation-settings: "MONO" var(--vf-mono)
                           "CASL" var(--vf-casl)
                           "slnt" var(--vf-slnt)
                           "ital" var(--vf-ital);
}

in later elements, we update only the values that we want to change. For example, if we want an element to use the monospaced font we could do something like this:

pre, code, pre code {
  --vf-mono: 1;
  font-variation-settings: "MONO" var(--vf-mono)
                           "CASL" var(--vf-casl)
                           "slnt" var(--vf-slnt)
                           "ital" var(--vf-ital);
}

Note that since we defined font variation settings on the :root element with the default values, this technique forces you to define the values and font-variation-settings on all elements that don’t use the default.

If we were not using variables then we’d have to use the bare metal approach which we’ll discuss below.

Bare Metal

Using variables address the cascade issues with font-variation-settings but there may be times when we don’t care about the extra work that we need to do.

Let’s say, for example, that we want to tweak strong and b to make it slightly less bold than the default value, we could redefine it like this:

strong,
b {
  font-weight: 600;
  font-variation-settings: "MONO" 0,
    "CASL" 0,
    "slnt" 0,
    "ital" 0.5;
}

This technique also allows for customizing the classes and how they use the font. Expanding on the previous example we can use font-variation-settings to combine slant and italics axes for the same text rather than using a single axis.

em,
i {
  font-weight: 400;
  font-variation-settings: "MONO" 0,
    "CASL" 0,
    "slnt" -15,
    "ital" 1;
}

The block below, from Prism.js default stylesheet, incorporates Recursive as the default monospaced font for all code fenced blocks that Prism handles.

code[class*="language-"],
pre[class*="language-"] {
  font-family: Recursive, Consolas, Monaco,
    'Andale Mono', 'Ubuntu Mono', monospace;
  font-size: 1em;

  /* Solarized light base00 */
  color: #657b83;
  text-align: left;
  white-space: pre;
  word-spacing: normal;
  word-break: normal;
  word-wrap: normal;

  /* Variable font settings */
  font-weight: 400;
  font-variation-settings: "MONO" 1,
    "CASL" 0,
    "slnt" 0,
    "ital" 0.5;

  line-height: 1.5;

  /* Change tab size to 2 */
  tab-size: 2;

  /* Control hyphenation */
  hyphens: none;
}

These are a few examples of what you can do with the font. You can also combine different approaches to create an even more flexible solution.

Recursive also presents some interesting characteristics that make it fun to play and experiment with. Let’s see how far we can push the technology.

Custom Material Design Typography

In a previous post I created a page using Material Design’s default typography classes and the fonts they are designed to work with. What I didn’t realize is that you can also create custom typographical systems using SASS and the existing typographical infrastructure for material design. I will also explore whether Recursive works well with Material Design

Getting started

Because we’re using a variable font with custom axes we need to define the default values in the stylesheet’s :root element and then use custom properties to handle inheritance problems in variable fonts as documented in Boiling eggs and fixing the variable font inheritance problem

We first define the font using the extended @font-face syntax for variable fonts as explained in MDN’s Variable Fonts Guide.

Note that the latest version of the Google Fonts API supports a small set of variable fonts. See Variable fonts & the new Google Fonts API and the initial announcement in Codepen for more information on how to use the new API. Also, note that it’s not official yet so don’t use it in production.

@font-face {
  font-family: 'Recursive';
  src:
    url('../fonts/recursive-2019_11_22-20_38.woff2') format('woff2-variations');
  font-weight: 300 1000;
}

Using the extended @font-face syntax we tell the browser that we’re loading a variable font using woff2-variations as the format.

We also specify the font-weight range to be from 300 to 1000. We’ll leverage this later when we set up classes for predefined instances.

In the :root element we use classes and variables to handle Open Type features. This section is taken from Wakamaifondue stylesheet for Cursive B025 (latest release when the post was written)

The final

:root {
  --recursive-aalt: "aalt"off;
  --recursive-case: "case"off;
  --recursive-dlig: "dlig"off;
  --recursive-dnom: "dnom"off;
  --recursive-frac: "frac"off;
  --recursive-numr: "numr"off;
  --recursive-ordn: "ordn"off;
  --recursive-pnum: "pnum"off;
  --recursive-sinf: "sinf"off;
  --recursive-ss01: "ss01"off;
  --recursive-ss02: "ss02"off;
  --recursive-ss03: "ss03"off;
  --recursive-ss04: "ss04"off;
  --recursive-ss05: "ss05"off;
  --recursive-ss06: "ss06"off;
  --recursive-ss07: "ss07"off;
  --recursive-ss08: "ss08"off;
  --recursive-ss09: "ss09"off;
  --recursive-ss10: "ss10"off;
  --recursive-ss11: "ss11"off;
  --recursive-ss20: "ss20"off;
  --recursive-sups: "sups"off;
  --recursive-titl: "titl"off;
  --recursive-zero: "zero"off;
}

We then create classes for each OpenType feature. If class is applied, update the custom property and apply modern font-variant-* when supported.

In this case, it is safe to use @supports because if the browser supports variable fonts I feel coonfident that it supports feature queries.

The final block of this section takes all the values of the OpenType variables and sets them appropriately using font-variation-settings

.recursive-aalt {
  --recursive-aalt: "aalt"on;
}

.recursive-case {
  --recursive-case: "case"on;
}

.recursive-dlig {
  --recursive-dlig: "dlig"on;
}

@supports (font-variant-ligatures: discretionary-ligatures) {
  .recursive-dlig {
    --recursive-dlig: "____";
    font-variant-ligatures: discretionary-ligatures;
  }
}

.recursive-dnom {
  --recursive-dnom: "dnom"on;
}

.recursive-frac {
  --recursive-frac: "frac"on;
}

@supports (font-variant-numeric: diagonal-fractions) {
  .recursive-frac {
    --recursive-frac: "____";
    font-variant-numeric: diagonal-fractions;
  }
}

.recursive-numr {
  --recursive-numr: "numr"on;
}

.recursive-ordn {
  --recursive-ordn: "ordn"on;
}

@supports (font-variant-numeric: ordinal) {
  .recursive-ordn {
    --recursive-ordn: "____";
    font-variant-numeric: ordinal;
  }
}

.recursive-pnum {
  --recursive-pnum: "pnum"on;
}

@supports (font-variant-numeric: proportional-nums) {
  .recursive-pnum {
    --recursive-pnum: "____";
    font-variant-numeric: proportional-nums;
  }
}

.recursive-sinf {
  --recursive-sinf: "sinf"on;
}

.recursive-ss01 {
  --recursive-ss01: "ss01"on;
}

.recursive-ss02 {
  --recursive-ss02: "ss02"on;
}

.recursive-ss03 {
  --recursive-ss03: "ss03"on;
}

.recursive-ss04 {
  --recursive-ss04: "ss04"on;
}

.recursive-ss05 {
  --recursive-ss05: "ss05"on;
}

.recursive-ss06 {
  --recursive-ss06: "ss06"on;
}

.recursive-ss07 {
  --recursive-ss07: "ss07"on;
}

.recursive-ss08 {
  --recursive-ss08: "ss08"on;
}

.recursive-ss09 {
  --recursive-ss09: "ss09"on;
}

.recursive-ss10 {
  --recursive-ss10: "ss10"on;
}

.recursive-ss11 {
  --recursive-ss11: "ss11"on;
}

.recursive-ss20 {
  --recursive-ss20: "ss20"on;
}

.recursive-sups {
  --recursive-sups: "sups"on;
}

@supports (font-variant-position: super) {
  .recursive-sups {
    --recursive-sups: "____";
    font-variant-position: super;
  }
}

.recursive-titl {
  --recursive-titl: "titl"on;
}

@supports (font-variant-caps: titling-caps) {
  .recursive-titl {
    --recursive-titl: "____";
    font-variant-caps: titling-caps;
  }
}

.recursive-zero {
  --recursive-zero: "zero"on;
}

@supports (font-variant-numeric: slashed-zero) {
  .recursive-zero {
    --recursive-zero: "____";
    font-variant-numeric: slashed-zero;
  }
}

.recursive-aalt,
.recursive-case,
.recursive-dlig,
.recursive-dnom,
.recursive-frac,
.recursive-numr,
.recursive-ordn,
.recursive-pnum,
.recursive-sinf,
.recursive-ss01,
.recursive-ss02,
.recursive-ss03,
.recursive-ss04,
.recursive-ss05,
.recursive-ss06,
.recursive-ss07,
.recursive-ss08,
.recursive-ss09,
.recursive-ss10,
.recursive-ss11,
.recursive-ss20,
.recursive-sups,
.recursive-titl,
.recursive-zero {
  font-feature-settings: var(--recursive-aalt),
    var(--recursive-case), var(--recursive-dlig), var(--recursive-dnom), var(--recursive-frac), var(--recursive-numr), var(--recursive-ordn), var(--recursive-pnum), var(--recursive-sinf), var(--recursive-ss01), var(--recursive-ss02), var(--recursive-ss03), var(--recursive-ss04), var(--recursive-ss05), var(--recursive-ss06), var(--recursive-ss07), var(--recursive-ss08), var(--recursive-ss09), var(--recursive-ss10), var(--recursive-ss11), var(--recursive-ss20), var(--recursive-sups), var(--recursive-titl), var(--recursive-zero);
}

The next block uses pre-defined instances of the Recursive font. Because the values are exclusive to the instance I didn’t think it necessary to deefine them using CSS variables. It’s possible but too time consuming for me.

font-weight is a predefined axis for variable fonts and it’s supported well enough that we can take it out of font-variation-settings and use the CSS property on its own.

Inside font-variation-settings, the uppercased axes (MONO and CASL) are custom axes to the font we’re using so they will always need to go here as there are no equivalent CSS properties.

The lowercased axes (slnt and ital) are predefined axes but need to go inside font-variation-settings to avoid confusion.

These are examples of the instance classes produced by Wakamaifondue. I didn’t want to list all 64 instances here 🙂

// Variable instances.
.recursive-mono-linear {
  font-weight:  400;
  font-variation-settings: "MONO"1, "CASL"0, "slnt"0, "ital"0.5;
}

.recursive-mono-linear-italic {
  font-weight:  400;
  font-variation-settings: "MONO"1, "CASL"0, "slnt"-15, "ital"1;
}

.recursive-mono-casual {
  font-weight:  400;
  font-variation-settings: "MONO"1, "CASL"1, "slnt"0, "ital"0.5;
}

.recursive-mono-casual-italic {
  font-weight:  400;
  font-variation-settings: "MONO"1, "CASL"1, "slnt"-15, "ital"1;
}

After all the work setting Open Type features and variable font instances we can work with Material Design using our custom font.

We import the SCSS files for typography and grid to include in the final result.

The last thing we do is to override the base font family to use Recursive. Because the variable font can be either sans serif or monospaced you will have to override the font family everywhere the font is monospaced.

@import "@material/typography/mdc-typography";
@import "@material/layout-grid/mdc-layout-grid";

$mdc-typography-font-family: unquote("Recursive, Arial, Helvetica");

You can see an example in action here: Towards the Splendid City and the code is here

Playing with underline styles

When working with underlining content other than links on the web be mindful of user expectations and provide some way to distinguish links from other types of underlined content.

First, we had links that were underlined. Next, we had links and the capability to underline text with the u element.

Then we wanted more control of what we could do with underlines so we moved to use border-bottom for our underlining playground because it gave us more flexibility than what the native underlining tools gave us at the time.

But now Firefox has introduced new text-decoration and text-underline rules that make it easier to customize our underlines to do what we want to do.

As far as I know and tested, text-decoration-thickness and text-underline-offset are Firefox only for now.

a {
  text-decoration-thickness: 3px; // 1
  text-underline-offset: 6px; // 2
  color: #ff2908; // 3
  text-decoration-color: rebeccapurple; // 4
  text-decoration-skip-ink: auto; // 5
  text-decoration-style: solid; // 6
}

The idea is that we can declare rules to better control underlines and it’s characteristics. The rules do the following:

  1. text-decoration-thickness sets the thickness of the underline
  2. text-underline-offset sets the offset distance of an underline line from its original position
  3. color determines the color of the link
  4. text-decoration-color sets the color of decorations
  5. text-decoration-skip-ink controls the behavior of underline and overline (but not line-through) when the line passes above the top or hang below the bottom of a line.
  6. text-decoration-style indicates what type of line to use in the underline

I don’t think we need to wrap this in a feature query since the browsers that don’t understand thickness and offset will ignore them but keep the ones it understands. So now we have a way to progressively enhance the way our underlined elements look without resorting to hacks.

CSS List Markers

I’ve always been curious about how people create customized list marker items, the bullets or numbers that appear in lists for webpages.

Firefox shipped an easier way to customize these lists using ::marker pseudo-elements.

The following example replaces all the markers for an unordered list with ✅. It also changes the text of the default markers so that if the emoji cannot be displayed for some reason, the default markers will be customized without the emoji.

We add a second selector for the li element without the marker so we can introduce space between the marker and the content to the item.

@supports selector(::marker) {
  ul li::marker {
    color: rebeccapurple;
    font-family: cursive;
    font-size: 2em;
    font-weight: 900;
    content: '✅';
  }

  ul li {
    padding-left: 1em;
  }
}

The example is wrapped in a selector feature query to make sure that markers are supported without making a mess off the existing design.

Jason Pamental’s Responsive Web Typography Newsletter issue 33 proposes an alternative that uses counters and ::before pseudo elements to accomplish a similar effect.

ol {
  list-style: none;
  counter-reset: list-counter;
}

ol li {
  font-family: Merriweather, serif;
  counter-increment: list-counter;
}

ol li::before {
  color: #c05522;
  display: block;
  float: left;
  font-family: Montserrat, sans-serif;
  margin-left: -1.25em;
  content: counter(list-counter) ". ";
}

The main differences are:

The second example removes the number marker before inserting content with the ::before pseudo-element.

The example without markers uses counters to increase their value. This would make continuation lists where we start a list from values other than 1 problematic.

Make sure that you test the solution with accessibility tools to make sure it doesn’t cause any problems.