Nuclear Anchored Sidenotes
Published 1 year, 2 months pastExactly one year ago today, which I swear is a coincidence I only noticed as I prepared to publish this, I posted an article on how I coded the footnotes for The Effects of Nuclear Weapons. In that piece, I mentioned that the footnotes I ended up using weren’t what I had hoped to create when the project first started. As I said in the original post:
Originally I had thought about putting footnotes off to one side in desktop views, such as in the right-hand grid gutter. After playing with some rough prototypes, I realized this wasn’t going to go the way I wanted it to…
I came back to this in my post “CSS Wish List 2023”, when I talked about anchor(ed) positioning. The ideal, which wasn’t really possible a year ago without a bunch of scripting, was to have the footnotes arranged structurally as endnotes, which we did, but in a way that I could place the notes as sidenotes, next to the footnote reference, when there was enough space to show them.
As it happens, that’s still not really possible without a lot of scripting today, unless you have:
- A recent (as of late 2023) version of Chrome
- With the “Experimental web features” flag enabled
With those things in place, you get experimental support for CSS anchor positioning, which lets you absolutely position an element in relation to any other element, anywhere in the DOM, essentially regardless of their markup relationship to each other, as long as they conform to a short set of constraints related to their containing blocks. You could reveal an embedded stylesheet and then position it next to the bit of markup it styles!
Anchoring Sidenotes
More relevantly to The Effects of Nuclear Weapons, I can enhance the desktop browsing experience by turning the popup footnotes into Tufte-style static sidenotes. So, for example, I can style the list items that contain the footnotes like this:
.endnotes li {
position: absolute;
top: anchor(top);
bottom: auto;
left: calc(anchor(--main right) + 0.5em);
max-width: 23em;
}
Let me break that down. The position
is absolute
, and bottom
is set to auto
to override a previous bit of styling that’s needed in cases where a footnote isn’t being anchored. I also decided to restrain the maximum width of a sidenote to 23em
, for no other reason than it looked right to me.
(A brief side note, pun absolutely intended: I’m using the physical-direction property top
because the logical-direction equivalent in this context, inset-block-start
, only gained full desktop cross-browser support a couple of years ago, and that’s only true if you ignore IE11’s existence, plus it arrived in several mobile browsers only this year, and I still fret about those kinds of things. Since this is desktop-centric styling, I should probably set a calendar reminder to fix these at some point in the future. Anyway, see MDN’s entry for more.)
Now for the new and unfamiliar parts.
top: anchor(top);
This sets the position of the top edge of the list item to be aligned with the top edge of its anchor’s box. What is a footnote’s anchor? It’s the corresponding superscripted footnote mark embedded in the text. How does the CSS know that? Well, the way I set things up — and this is not the only option for defining an anchor, but it’s the option that worked in this use case — the anchor is defined in the markup itself. Here’s what a footnote mark and its associated footnote look like, markup-wise.
explosion,<sup><a href="#fnote01" id="fn01">1</a></sup> although
<li id="fnote01" anchor="fn01"><sup>1</sup> … </li>
The important bits for anchor positioning are the id="fn01"
on the superscripted link, and the anchor="fn01"
on the list item: the latter establishes the element with an id
of fn01
as the anchor for the list item. Any element can have an anchor
attribute, thus creating what the CSS Anchor Positioning specification calls an implicit anchor. It’s explicit in the HTML, yes, but that makes it implicit to CSS, I guess. There’s even an implicit
keyword, so I could have written this in my CSS instead:
top: anchor(implicit top);
(There are ways to mark an element as an anchor and associate other elements with that anchor, without the need for any HTML. You don’t even need to have IDs in the HTML. I’ll get to that in a bit.)
Note that the superscripted link and the list item are just barely related, structurally speaking. Their closest ancestor element is the page’s single <main>
element, which is the link’s fourth-great-grandparent, and the list item’s third-great-grandparent. That’s okay! Much as a <label>
can be associated with an input element across DOM structures via its for
attribute, any element can be associated with an anchoring element via its anchor
attribute. In both cases, the value is an ID.
So anyway, that means the top edge of the endnote will be absolutely positioned to line up with the top edge of its anchor. Had I wanted the top of the endnote to line up with the bottom edge of the anchor, I would have said:
top: anchor(bottom);
But I didn’t. With the top edges aligned, I now needed to drop the endnote into the space outside the main content column, off to its right. At first, I did it like this:
left: anchor(--main right);
Wait. Before you think you can just automatically use HTML element names as anchor references, well, you can’t. That --main
is what CSS calls a dashed-ident, as in a dashed identifier, and I declared it elsewhere in my CSS. To wit:
main {
anchor-name: --main;
}
That assigns the anchor name --main
to the <main>
element in the CSS, no HTML attributes required. Using the name --main
to identify the <main>
element was me following the common practice of naming things for what they are. I could have called it --mainElement
or --elMain
or --main-column
or --content
or --josephine
or --📕😉
or whatever I wanted. It made the most sense to me to call it --main
, so that’s what I picked.
Having done that, I can use the edges of the <main>
element as positioning referents for any absolutely (or fixed) positioned element. Since I wanted the left side of sidenotes to be placed with respect to the right edge of the <main>
, I set their left
to be anchor(--main right)
.
Thus, taking these two declarations together, the top edge of a sidenote is positioned with respect to the top edge of its implicit anchor, and its left edge is positioned with respect to the right edge of the anchor named --main
.
top: anchor(top);
left: anchor(--main right);
Yes, I’m anchoring the sidenotes with respect to two completely different anchors, one of which is a descendant of the other. That’s okay! You can do that! Literally, you could position each edge of an anchored element to a separate anchor, regardless of how they relate to each other structurally.
Once I previewed the result of those declarations, I saw I the sidenotes were too close to the main content, which makes sense: I had made the edges adjacent to each other.
I thought about using a left margin on the sidenotes to push them over, and that would work fine, but I figured what the heck, CSS has calculation functions and anchor functions can go inside them, and any engine supporting anchor positioning will also support calc()
, so why not? Thus:
left: calc(anchor(--main right) + 0.5em);
I wrapped those in a media query that only turned the footnotes into sidenotes at or above a certain viewport width, and wrapped that in a feature query so as to keep the styles away from non-anchor-position-understanding browsers, and I had the solution I’d envisioned at the beginning of the project!
Except I didn’t.
Fixing Proximate Overlap
What I’d done was fine as long as the footnotes were well separated. Remember, these are absolutely positioned elements, so they’re out of the document flow. Since we still don’t have CSS Exclusions, there needs to be a way to deal with situations where there are two footnotes close to each other. Without it, you get this sort of thing.
I couldn’t figure out how to fix this problem, so I did what you do these days, which is I posted my problem to social media. Pretty quickly, I got a reply from the brilliant Roman Komarov, pointing me at a Codepen that showed how to do what I needed, plus some very cool highlighting techniques. I forked it so I could strip it down to the essentials, which is all I really needed for my use case, and also have some hope of understanding it.
Once I’d worked through it all and applied the results to TEoNW, I got exactly what I was after.
But how? It goes like this:
.endnotes li {
position: absolute;
anchor-name: --sidenote;
top: max(anchor(top) , calc(anchor(--sidenote bottom) + 0.67em));
bottom: auto;
left: calc(anchor(--main right) + 0.5em);
max-width: 23em;
}
Whoa. That’s a lot of functions working together there in the top
value. (CSS is becoming more and more functional, which I feel some kind of way about.) It can all be verbalized as, “the position of the top edge of the list item is either the same as the top edge of its anchor, or two-thirds of an em below the bottom edge of the previous sidenote, whichever is further down”.
The browser knows how to do this because the list items have all been given an anchor-name
of --sidenote
(again, that could be anything, I just picked what made sense to me). That means every one of the endnote list items will have that anchor name, and other things can be positioned against them.
Those styles mean that I have multiple elements bearing the same anchor name, though. When any sidenote is positioned with respect to that anchor name, it has to pick just one of the anchors. The specification says the named anchor that occurs most recently before the thing you’re positioning is what wins. Given my setup, this means an anchored sidenote will use the previous sidenote as the anchor for its top edge.
At least, it will use the previous sidenote as its anchor if the bottom of the previous sidenote (plus two-thirds of an em) is lower than the top edge of its implicit anchor. In a sense, every sidenote’s top edge has two anchors, and the max()
function picks which one is actually used in every case.
CSS, man.
Remember that all this is experimental, and the specification (and thus how anchor positioning works) could change. The best practices for accessibility are also not clear yet, from what I’ve been able to find. As such, this may not be something you want to deploy in production, even as a progressive enhancement. I’m holding off myself for the time being, which means none of the above is currently used in the published version of The Effects of Nuclear Weapons. If people are interested, I can create a Codepen to illustrate.
I do know this is something the CSS Working Group is working on pretty hard right now, so I have hopes that things will finalize soon and support will spread.
My thanks to Roman Komarov for his review of and feedback on this article. For more use cases of anchor positioning, see his lengthy (and quite lovely) article “Future CSS: Anchor Positioning”.