Making Hugo’s Footnotes Accessible

January 14, 2019

Note: The JavaScript examples found in this article use ES6+ code. I recommend transpiling this code to ES5 using Babel to improve support for older browsers.

As you might already know, I tend to focus on two main things when developing websites: performance and accessibility. This website is no exception, which is why I decided to use Hugo, an incredibly fast static site generator built in Go. Static sites are great for performance and security, and Hugo gives you control of almost all parts of the markup, which makes it easier to make your website as accessible as possible.

That being said, there’s a few parts of the markup that are rendered by Hugo and aren’t as easy to control. One of these parts is the footnotes, which are generated automatically by Hugo’s Markdown processor Blackfriday. In this article we’ll look at making these footnotes accessible in a few steps.1

What do we need to do?

We will be following the steps outlined in this excellent article by Hugo Giraudel. The HTML structure generated by Hugo (the static site generator, not the author of the aforementioned article) is already semantically correct, which means we can focus on the other parts. In short, here’s what we need to do:

  • Add a title for screen readers.
  • Describe our references with this title.
  • Add a label to the back-to-content links.
  • Bonus: provide a visual clue when jumping through the document.

Let’s get started!

Step 1: Add a title for screen readers

The first thing we need to do is add a title to our list of footnotes. This ensures screen reader users will understand what’s going on when they get to the footnotes section. Since we don’t have access to the HTML (it is generated automatically), we will use JavaScript to insert the title right before the list.

To get started, we need to figure out if the page we’re on contains footnotes. Hugo adds the footnotes class to a list of footnotes, which we can use to check whether or not our page contains footnotes:

const footnotes = document.querySelector('.footnotes');

if (footnotes) {
  // Aha, footnotes! Do our thing here.
}

This little snippet ensures our code will only run when necessary. Next, we declare some values that we will need later.

const title = 'Footnotes';
const id = 'footnotes-label';

Here, title is the text of the title that will be shown to screen readers, and id is the ID that will be assigned to this title, which we can use to reference to our list of footnotes from an individual footnote.

With these variables declared, we can add the title to our list of footnotes:

const element = document.createElement('h2');
const text = document.createTextNode(title);

element.appendChild(text);
element.classList.add('u-hidden-visually');
element.id = id;

footnotes.insertBefore(element, footnotes.firstChild);

This code creates a new <h2> element, and fills it with the title text that we declared earlier. It also adds the ID and the u-hidden-visually class to the element. Finally, it inserts the element right before the list of footnotes.

This results in the following HTML:

<h2 class="u-hidden-visually" id="footnotes-label">Footnotes</h2>

Perfect!

The u-hidden-visually class is a so-called utility class. It follows the single responsibility principle, and has a very specific task. In this case, any element with this class will be available to screen readers but invisible on actual screens. To accomplish this, we can use the following CSS:

.u-hidden-visually {
  clip-path: inset(100%);
  clip: rect(1px, 1px, 1px, 1px);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

Step 2: Describe our references with this title

Next up, we want to describe all of our footnote links with this title, using the aria-describedby attribute. When a screen reader gets to a link referencing a footnote, it will read out this title.

I’ve written a little helper function that sets an attribute for each item that matches the selector:

/**
 * Set attribute value for given selector.
 *
 * @param {String} selector - Selector to set attribute for.
 * @param {String} attribute - Attribute to set.
 * @param {String} value - Value for the attribute.
 */
const setAttributeValue = ({ selector, attribute, value }) => {
  if (!selector || !attribute || !value) {
    return;
  }

  const items = document.querySelectorAll(selector);

  if (!items.length) {
    return;
  }

  for (const item of items) {
    item.setAttribute(attribute, value);
  }
};

We can use this function to add the correct aria-describedby value to each footnote link, using the ID of the title that we declared earlier:

setAttributeValue({
  selector: '.footnote-ref a',
  attribute: 'aria-describedby',
  value: id
});

Note: for the parameters of this function I’ve used a cool little trick called parameter destructuring. It makes it very easy to pass parameters to a function using an object. Read more about it here.

The last thing we need to do is add the appropriate label to the back-to-content links. These links are useful for everyone but crucial for screen reader users, as these links allow them to jump back to where they came from in a single click, instead of going backwards through the text.

Hugo provides these back-to-content links by default. All we need to do is add an appropriate label for screen readers. Back to content will do just fine in this case, and we can use the same function we used earlier:

setAttributeValue({
  selector: '.footnote-return',
  attribute: 'aria-label',
  value: 'Back to content'
});

That’s it! Our footnotes are now fully accessible. I’ve posted the full code at the bottom of this article—you can copy-paste it into your Hugo website and it should work out of the box.

Bonus: provide a visual clue when jumping through the document

There’s one extra thing I like to add. When clicking on a reference to a footnote, the page will jump straight down to this footnote. This can be quite confusing for screen users, as they might not be sure where to look. To help with this, I like to add a visual clue to the footnote.

Fortunately, we can manage this with CSS. When clicking on a footnote reference, the browser automatically targets the footnote (this is what causes the page to jump to the footnote). We can take advantage of this by using the :target attribute in our CSS to highlight the footnote.

The code below creates an invisible pseudo-element with a yellow gradient background underneath the footnote. When the footnote gets focused, the background will appear and then slowly fade away. Try it yourself by clicking on the footnote right here → 2

@keyframes highlight {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

.footnotes li,
.footnote-ref {
  position: relative;
}

.footnotes li::after,
.footnote-ref::after {
  position: absolute;
  z-index: -1;
  content: '';
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(
    -100deg,
    rgba(#fae885, 0.3),
    rgba(#fae885, 0.6) 95%,
    rgba(#fae885, 0.1)
  );
  opacity: 0;
}

.footnotes li:target::after,
.footnotes li:focus::after,
.footnote-ref:target::after,
.footnote-ref:focus::after {
  animation: highlight 2s cubic-bezier(0.3, 0, 1, 0.7);
}

This code could be simplified by using a preprocessor like Sass, but I’ve decided to use a plain CSS example here to make it usable for as many people as possible. Here’s the Sass version.

Full code

Here’s the full code for our accessible footnotes. I’ve added a few extra comments to clarify the different steps that are taken.

const footnotes = document.querySelector('.footnotes');

// Only run this code if there are footnotes on the page.
if (footnotes) {
  /**
   * Set attribute value for given selector.
   *
   * @param {String} selector - Selector to set attribute for.
   * @param {String} attribute - Attribute to set.
   * @param {String} value - Value for the attribute.
   */
  const setAttributeValue = ({ selector, attribute, value }) => {
    if (!selector || !attribute || !value) {
      return;
    }

    const items = document.querySelectorAll(selector);

    if (!items.length) {
      return;
    }

    for (const item of items) {
      item.setAttribute(attribute, value);
    }
  };
  const title = 'Footnotes';
  const id = 'footnotes-label';

  // Create an <h2> element and add it to the beginning of the .footnotes element.
  const element = document.createElement('h2');
  const text = document.createTextNode(title);

  element.appendChild(text);
  element.classList.add('u-hidden-visually');
  element.id = id;

  footnotes.insertBefore(element, footnotes.firstChild);

  // Use the footnotes title to describe each reference.
  setAttributeValue({
    selector: '.footnote-ref a',
    attribute: 'aria-describedby',
    value: id
  });

  // Add a 'Back to content' label to each back-to-content link.
  setAttributeValue({
    selector: '.footnote-return',
    attribute: 'aria-label',
    value: 'Back to content'
  });
}

If you want the title to be hidden for users not using a screen reader, don’t forget to add this CSS:

.u-hidden-visually {
  clip-path: inset(100%);
  clip: rect(1px, 1px, 1px, 1px);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

Disclaimer: the version I use on this website is slightly different, because I’ve set it up as an ES6 module. You can find it here, and use it like this:

import accessibleFootnotes from './accessibleFootnotes';

accessibleFootnotes();

  1. For obvious reasons, I had to include a footnote in this article. You can inspect the code of this footnote using DevTools, and see the screen reader title and ARIA attributes for yourself.
  2. Did you see that? It also works on the return links →
Do you have any questions or comments about this post? Tweet me at @danielpost! I am also available for hire!