This website now uses margin notes instead of footnotes if your browser window is wide enough1. 🎉 If you’re reading this on a computer, you’ll probably be able to see them in this article if you maximise your browser window, and if you still can’t see them, you can try zooming out! If you’re on a mobile or an HTML5-compliant smart fridge (whyyyyy), there’s probably no hope for you, so here’s a video of the effect in action:

There will be an awful lot of footnotes in this article because of (a) unbridled enthusiasm for a cool feature I just shipped (b) I need a test suite for pathological cases. Sorry2 in advance.

Why Margin Notes?

A couple of weeks ago, I read an essay expounding the virtues of footnotes, in which Veit Heller writes:

I personally enjoy sidenotes – some call them marginalia – very much. They’re unobtrusive but close enough to the text to not break coherence…

[but] you might have noticed that I don’t use sidenotes on this blog. There’s a good reason for that: making sidenotes responsive is not easy.

Well, yes, I agree, but… well, maybe it’s not that hard? And maybe it would look cool? This idea kinda stuck with me, and then on Saturday morning, I decided to give it a shot.

I’m still not sure why I made that decision! It took about an hour to get the initial idea sorta working, and then another five (😅) to get it polished and working consistently. Here’s how I made it work.

Absolute Positioning?

At first, I thought I’d try absolute positioning. The premise is simple:

This approach is enough to get something working! But it’s got a number of problems:

The overlap issue is demonstrated by this example from The Bagel Code, which has a lot of footnotes in one paragraph to give references for a few interlocking ideas:

screenshot of article showing a jumble of overlapping text in the margin notes

Oh. :(

Despite the issues, I was pretty bought in to the idea by this point. So I started looking for other possible solutions.

Drawing inspiration

I remembered that sometime in 2011 I read Bret Victor’s Magic Ink, and in addition to being super impressed with the content, I had apparently noted that it uses margin notes6. Maybe I could just lift Bret’s HTML+CSS implementation7?

Turns out Bret’s using this same positioning technique! And then has just been super careful about not adding too many in quick succession. I didn’t want that limitation, and also, I wanted to be able to share this with people as part of my Hugo theme, and I don’t like having gotchas in the code that I ship.

It sounds like margin notes are going to have to be aware of two things:

This is the point at which I was pretty sure I wouldn’t be able to do the layout with CSS. It was time to break out the Big Guns:

A screenshot of a tweet, which says: That feeling when you're trying to align two divs and then succeed but realise you've written 70 lines of javascript

Doing it with Javascript

In the end, I did the positioning with Javascript. The idea is pretty straightforward, but ironing out the edge-cases gets technical quickly:

  1. Assume non-floating footnotes by default. This is what we already have, and it will continue to be the small-screen experience.
  2. Make a CSS class, .floating-footnotes, which repositions the entire .footnotes section to the right of the article content, using the same absolute positioning technique as before.
  3. When the script first runs, or the article content reflows:
    1. Check if the width of the page is ‘wide enough’
    2. Add the .floating-footnotes class to the .footnotes section, to trigger the repositioning to the right of the article content
    3. Compute a target vertical position for each footnote such that it’s aligned with its in-text reference, and so that it doesn’t overlap any previous footnote
    4. Set position: absolute and top: XXXpx on the individual footnotes to meet these vertical position goals.

Of these steps, the two that were surprisingly complex were aligning the individual footnotes, and watching for article reflows. Let’s talk about those now.

Aligning individual footnotes

This felt complex because I needed to ensure that I fully understood CSS positioning to make it work.

Here’s the code to compute the offset for a given footnote:

// Computes an offset such that setting `top` on `footnote` will put it
// in vertical alignment with targetAlignment.
function computeOffsetForAlignment(footnote, targetAlignment) {
    const offsetParentTop =
        footnote.offsetParent.getBoundingClientRect().top;
    // Distance between the top of the offset parent and the top of the
    // target alignment
    return
        targetAlignment.getBoundingClientRect().top - offsetParentTop;
}

There’s a lot packed into the two lines in that method:

It’s probably easier to represent this visually, so I drew this diagram:

Diagram showing the relationship between offsets on the page

Once you’ve made this computation, you need to add collision-avoidance code. Mine works by keeping track of the offset of the bottom of the last element (bottomOfLastElem):

if (offset < bottomOfLastElem) {
    offset = bottomOfLastElem;
}
// Set this so that the next element doesn't clobber this one
bottomOfLastElem =
    // calculated offset for this footnote
    offset +
    // height of this footnote, including padding
    footnote.offsetHeight +
    // margins for this footnote. Have to parse them to int because
    // they're always represented in pixels with the `px` suffix.
    parseInt(window.getComputedStyle(footnote).marginBottom) +
    parseInt(window.getComputedStyle(footnote).marginTop);

In the end, I decided to align the margin notes to the top of the containing paragraph, not to the top of the actual footnote. I think that this looks a lot cleaner. Most of the time, my paragraphs should be short enough that it’s not disconcerting to have a margin note slightly above its in-text reference.

Watching for article reflows

I found that the above alignment strategy mostly worked well, except for articles that had embedded tweets.

I had a theory that the embedded tweets were causing content to move around in the article, which meant that I’d need to recompute the position of the footnotes after they’d finished loading. It’s possible to watch for changes in the DOM, but I don’t think that can detect when an iframe has resized because it’s finished loading, which is what was causing this specific problem.

Footnotes are converted to margin notes once the page finishes loading, and then the browser asynchronously loads the iframe containing the tweet, which throws everything out of alignment.

In the end, I stumbled across ResizeObserver, which allows you to monitor changes in the size of an element8. It seemed promising, but it’s not (as of Jan 2020) widely available. There’s a few polyfills available for it though! I grabbed this one and tried to pull it in to my Javascript file… but the readme mentions modules everywhere.

Hmmm, modules. Aren’t they a Node JS thing?

I’d been so keen to avoid the whole Javascript Package Manager / toolchain / install a million packages from NPM thing. But, ok, maybe it’s not so bad; let’s look for something lightweight that does the trick.

A Javascript toolchain for people who don’t like Javascript toolchains

The first question I ran into was: what exactly do I need to install?

I knew I’d need a package manager to install the @juggle/resize-observer package. I guess normally you’d just use NPM for that? At some point in my exploration I read this comparison and decided to take a look at Yarn, an alternative frontend to the NPM repository. The Getting Started guide was extremely straightforward, and so I guess I ended up using Yarn because it Just Worked9.

The second question: I’ve now got multiple JS files, and that module that I installed from NPM with Yarn. How do I bundle all these together?

You can see my thought process through my Firefox history:

At some point I just noped out and googled “simplest way to bundle frontend code in 2020”10, and Google delivered! I chose Parcel, which explicitly promises zero configuration.

I’m usually a bit wary of ‘zero-config’ claims, and in this case, it turns out you set all the options via the command-line 🙃 but, if you don’t, it still works11, and the command’s not that complex, so it’s ok.

So now, to package up all the JS for the application, I run

parcel build \
  --no-source-maps --experimental-scope-hoisting --out-dir static/js \
  js/main.js

# This works fine too; it makes the build ~2kB bigger
parcel build --out-dir static/js js/main.js

Available now in the paperesque Hugo theme

I’ve bundled all this up into the paperesque Hugo theme, which is what I’m using on this site. I’m super happy with how this feature turned out 🥳. You can get the source on Github or check it out on Hugo Themes.


  1. Specifically, ‘wider than 1260 pixels’. ↩︎

  2. Not sorry. ↩︎

  3. Here’s an example of lots of footnotes in quick succession. ↩︎

  4. Is this overusing footnotes? Probably! It’s great for looking smart, and not so great for communicating ideas. But sometimes it’s hard to stop yourself, or you need to cite a lot of references in quick succession. ↩︎

  5. You’ll notice that I figured out how to get around this ‘overlap’ issue. We’ll talk about how shortly. ↩︎

  6. And it turns out he in turn borrowed the margin notes (and lots of other ideas) from Edward Tufte’s books, which Veit also references in his essay. ↩︎

  7. Also it’s pretty wild to me that the same HTML/CSS works as well in 2020 as it did in 2006, when Magic Ink was written. The world-wide web is one of the best examples of long-term compatibility we have in software development (along with Microsoft Windows, which can still run binaries from 1995). ↩︎

  8. Currently, the code watches the size of the article content div to see whether a reflow is required. This isn’t perfect – in theory, the relevant container could stay the same height while two inner elements changed at the same time, which would potentially not trigger the resize. I imagine that situation is extremely rare for Hugo sites, though, so I think we’re probably safe. ↩︎

  9. “It Just Worked” is a huge testament to the Yarn developers and documentation team. I’ve got a newfound appreciation for how difficult this is after shipping Grouse and getting bug reports from users whose sites are only almost supported. ↩︎

  10. Admittedly, a risky move considering it’s still January. ↩︎

  11. I like this philosophy! ‘Just Works’ out of the box, has options if you need them. Again, this is really simple to say, and really hard to get right. ↩︎