footnotes on the web

published on the 1st of Nov, 2019

Note This article uses the very technique described. Feel free to Inspect along!

I recently found a nifty :target trick by CSS-Tricks and decided to play around with the technique. Googling for other footnote techniques, I stumbled upon a Sitepoint article that uses CSS counters. This article is about putting those two techniques together (with a bit of my own flair ontop).

1. The :target trick

The gist of it is to use regular anchor links (<a>) and the browser's ability to scroll to a matching fragment ID. We can then highlight the clicked footnote via the :target CSS pseudo-class. This pseudo-class selector matches an element with an id matching the URL's fragment. Imagine that your site has an /#about-section anchor. Once a user has clicked on a link to take them to that anchor, the :target selector's styles will be applied.

Demo 👇

Some link to an imaginary Section 42. Notice how it will be styled via the :target pseudo-class once clicked.

<a href="#target-styles-demo">Section 42</a>

<!-- content... -->

<p id="target-styles-demo">[Section 42]</p>
#target-styles-demo:target {
  background-color: cornflowerblue;
  color: white;
}

[Section 42]

Reset the demo

We can then leverage this "active fragment" styling to highlight the relevant footnote.

2. The CSS counter

I really like this one, as I rarely see a great use-case for CSS counters outside lists and I always find them super nifty! I find that counters make perfect sense for footnotes and they almost seem made to fit for this very purpose.

We are assigning an aria-describedby attribute to our footnote references and then assigning our CSS counter to that. Free accessibility bonus! We then display the counter value in a pseudo-element, since a pseudo-element's content property can access the counter and display its value.

The attribute isn't mandatory here. If you wish to save on characters, you could also apply the counter by any class selector.

/* Let's create a counter on a wrapper element */
article {
  counter-reset: footnotes;
}
/* Here we increment the counter for every footnote reference */
a[aria-describedby="footnote-label"] {
  counter-increment: footnotes;
  text-decoration: none;
  color: inherit;
  outline: none;
}
/**
 * Actual numbered references
 * 1. Display the current state of the counter (e.g. `[1]`)
 * 2. Style text as superscript
 */
a[aria-describedby="footnote-label"]::after {
  content: '[' counter(footnotes) ']';
  vertical-align: super;
  font-size: 0.5em;
  margin-left: 2px;
  color: blue;
  text-decoration: underline;
  cursor: pointer;
}

a[aria-describedby="footnote-label"]:focus::after {
  outline: thin dotted;
  outline-offset: 2px;
}

Here's the HTML for the footnote references we sprinkle into our content (we give it an id so we could link back to it from the footnotes - providing the user a way to jump back right where they left off)

<a aria-describedby="footnote-label"
  id="cite-ref-1"
  href="#cite-1">Section 42</a>

3. Indicating active footnote

With almost all the pieces in place, we can now add a touch of magic so that the user would know what footnote they were taken to (and vice-versa when throwing them back into content).

If you haven't already experimented, click on this footnote to see the effect.

/* Inline footnotes */
a[aria-describedby="footnote-label"]:target {
  animation: highlight 3s;
}

/* Wrapper of your footnotes */
footer :target {
  animation: highlight 2.75s;
}

@keyframes highlight {
  from { outline: 10px solid cornflowerblue; }
  to { outline: 10px solid transparent; }
}

Bonus:

I'm including a "back to top" link with every footnote in our footer. For this to work, we'll also add an ID to the link in the content that references a footnote. I'm using a [cite-1, cite-ref-1] convention here because at first I had long descriptive names per footnote, but it got cumbersome to

  • type them out each time
  • try and avoid clashes
  • remember what I had just typed when creating the "Back to top" button in the footer

I saw that Wikipedia uses the same for their footnote fragments (without referencing back to the content)! And this will work just as well for GitHub's Markdown preview.

Laa-dee-daa and a <a href="#cite-1" id="cite-ref-1">scientific term</a>.

<!-- ...content -->

<footer>
  <p id="cite-1">Explanation of fancy term. <a href="#cite-ref-1">☝️ Back</a></p>
</footer>

Issues

Turns out SPAs have a hard time with this, since the HTML5 History API's pushState() method does not activate the :target selector's styles.

There is also a reported issue on Chromium and the behaviour seems consistent across browsers.

Remedies:

  • Changing the URL the old-fashioned way (window.location.href = 'page#some-hash') will trigger the styles
  • For Vue and React link components (e.g. <router-link>), you could add a click listener
<router-link
  to='/page#some-hash'
  @click.native="() => window.location.hash = '#some-hash'"/>

If you're using Saber as I am, you can add the saber-ignore property to your links.

Practicality

So it is cumbersome to write these in a Markdown file, as you'll need to whip them up in vanilla HTML every time. You might be able to get away with writing a Vue/React component for your Static Site Generator, or maybe a Markdown plugin to handle these.

After all, one of Markdown's hyperlink syntaxes is itself a reference mechanism:

[Link test][1]

[1]: https://en.wikipedia.org/wiki/Weissman_score

So maybe that could be cleverly transformed to Footnotes when the reference is not a valid link? 🤔 Maybe we'll explore that in the future together.

Even with the troublesome DX, I like the academic feel of properly referencing your statements.

If you'd wish to check out the demo code, this whole article is just a single Vue component! Browse the source


Footnotes:
  1. Right-click > Inspect will open up your browser's Developer tools (☝️ back up)
  2. :target is a CSS pseudo-class selector - MDN docs (☝️ back up)
  3. CSS counters are custom iterations of list markers MDN docs (☝️ back up)
  4. Well... what do you know? It worked ✨ (☝️ back up)
  5. This will tell Saber to not render them as <saber-link> components and will make them act as regular ol' anchor elements saber-link docs (☝️ back up)

Find me on

Proudly generated with Saber,

safely hosted on Netlify.