Making semantic sidenotes without JavaScript

One of the first things I learned about professional writing is that it’s best to remove all unnecessary words. Problem is that that can make a text rather dry. Sometimes I feel some background info or a joke is just too good to leave out. Sidenotes are a nice solution to not muddy an article and still leave some richness in there for the curious reader who’s not in a hurry.

Implementing sidenotes on the web isn’t entirely straightforward though. Semantically, sidenotes are like footnotes, but visually they’re not. The best choice of semantic elements may depend on the kind of content of the sidenotes. My previous post about sidenotes, discusses what HTML elements I consider eligible and which I have chosen for what reasons. After writing that post I’ve learned a few things. Also some commenters kindly showed how I could improve the implementation. In this post I want show you my new and improved approach. Now available without JavaScript!

What my sidenotes do

My (sidenote: By the way, here’s an example of an actual sidenote. It’s using the same techniques that I present below. ) have two parts:

  1. The sidenote’s content
  2. A word or a span of words that it refers to.

The example sidenote above shows exactly what I mean with that. The implementation has meets the following requirements:

  • On viewports not wide enough for margins to show sidenotes, the sidenote content should be hidden by default. A click/tap on the words referring to it should make the content visible.
  • The span that the sidenote refers to should have a proper semantic HTML element.
  • The content of the sidenote should have a similarly valid HTML tag. Additionally:
    • The sidenote content may contain span elements such as <a> and <em>.
    • The sidenote content may contain clickable elements that can receive keyboard focus.
    • The sidenote content must be stylable.
  • The elements should not cause auto-closing of their parent <p> tag.
  • Reader modes and apps like Pocket and RSS readers should show sidenote content in a typographically acceptable way. That means at the very least that I can’t rely on website’s CSS to place and style the elements correctly.
  • The sidenote content should be read by screen readers, in a flow that makes sense. That likely means the two parts should be be placed together.
  • No JavaScript should be required.

Solution

This solution is similar to Tufte CSS’s. That means that the visibility of the sidenote’s content for narrow viewports is toggled with a checkbox. I’m using different semantic elements than Tufte CSS though. And I add ‘(Sidenote: )’ around the sidenote’s content in a way that is only visible when CSS is not used. The main reason for it is that the flow of a sentence sounds just better like that on screenreaders.

<span class="sidenote">
  <input
    aria-label="Show sidenote"
    type="checkbox"
    id="sidenote__checkbox--1"
    class="sidenote__checkbox">
  <label
    tabindex="0"
    title=""
    aria-describedby="sidenote-1"
    for="sidenote__checkbox--1"
  >Sidenote label</label>
  <small
    id="sidenote-1"
    class="sidenote__content">
    <span class="sidenote__content-parenthesis"
    > (sidenote: </span>
    Sidenote content
    <span class="sidenote__content-parenthesis">)</span>
  </small>
</span>

There’s quite a bit of additional CSS necessary to make things look good, but here are the essential parts for positioning and toggling the visibility of the sidenote’s content:

@media screen and (max-width: 1079px){

  .sidenote__checkbox ~ .sidenote__content{
    /* Hidden, but accessible to browsers that don't do CSS (e.g. screenreaders, Pocket) */
    position: absolute;
    left: -99999px;
    top: auto;
  }
  .sidenote__checkbox:checked ~ .sidenote__content{
    position: relative; /* override hidden-but-accessible */
    left: auto; /* override hidden-but-accessible */

    display: block;
    margin: 0.8rem 0;
    padding: 0.8rem 1.6rem;
  }
}

@media screen and (min-width: 1080px){
  .sidenote__content{
    display: block;
    position: absolute;
    right: 0;
    margin-top: calc(-1.5*var(--text-size)); /* Align sidenote top with main text */
    margin-right: calc(-1*var(--sidenote-width) - 1*var(--sidenote-margin));
    width: var(--sidenote-width);
    font-size: var(--text-size);
    color: var(--text-color);
  }
}

You can find the full code and demo files in this open source repo: Sidenotes. Feel free to send pull requests!

Is that it?

Depending on the type of content you’re going to put in the sidenotes, you may use <aside> or <footer> instead of the <small> element that I picked.

In August 2020 Gwern published an overview of sidenote implementations. It contains a thorough analysis of challenges and solutions. There’s a big table comparing all solutions they researched, (sidenote: An honour, Gwern is one of the most amazing websites I know! ) . It’s definitely worth to take a look at it before implementing the above.

Because I didn’t like that Gwern marked my initial solution as ‘non-free’, I properly open sourced the solution I described in this post. Check out the repo for all a full demo.