Parenthetical Asidenotes
Published 7 hours pastIt’s not really a secret I have a thing for sidenotes, and thus for CSS anchored positioning. But a thing I realized about myself is that most of my sidenotes are likely to be tiny asides commenting on the main throughline of the text, as opposed to bibliographic references or other things that usually become actual footnotes or endnotes. The things I would sidenote currently get written as parenthetical inline comments (you know, like this). Asidenotes, if you will.
Once I had realized that, I wondered: could I set up a way to turn those parenthetical asides into asidenotes in supporting browsers, using only HTML and CSS? As it turns out, yes, though not in a way I would actually use. In fact, the thing I eventually arrived at is pretty terrible.
Okay, allow me to explain.
To be crystal clear about this, here’s how I would want one of these parenthetical asides to be rendered in browsers that don’t support anchored positioning, and then how to render in those that do (which are, as I write this, recent Chromium browsers and Safari Technology Previews; see theanchor() MDN page for the latest):
My thinking is, the parenthetical text should be the base state, with some HTML to flag the bit that’s an asidenote, and then CSS is applied in supporting browsers to lay out the text as an asidenote. There is a marker pair to allow an unambiguous association between the two, which is tricky, because that marker should not be in the base text, but should appear when styled.
I thought for a minute that I would wrap these little notes in <aside>s, but quickly realized that would probably be a bad idea for accessibility and other reasons. I mean, I could use CSS to cast the <aside> to an inline box instead of its browser-default block box, but I’d need to label each one separately, be very careful with roles, and so on and so on. It was just the wrong tool, it seemed to me. (Feel free to disagree with me in the comments!)
So, I started with this:
<span class="asidenote">(Feel free to disagree with me in the comments!)</span>
That wasn’t going to be enough, though, because I can certainly position this <span>, but there’s nothing available to leave a maker behind when I do! Given the intended result, then, there needs to be something in the not-positioned text that serves in that role (by which I mean a utility role, not an ARIA role). Here’s where my mind went:
<span class="asidenote">(by which I mean a utility role, not an ARIA role)</span><sup></sup>
The added <sup> is what will contain the marker text, like 1 or a or whatever.
This seemed like it was the minimum viable structure, so I started writing some styles. These asidenotes would be used in my posts, and I’d want the marker counters to reset with each blog post, so I built the selectors accordingly:
@supports not (anchor-name: --main) {
#thoughts article .asidenote + sup {
display: none;
}
}
@supports (anchor-name: --main) {
#thoughts {
anchor-name: --main;
}
#thoughts article {
counter-reset: asidenotes;
}
#thoughts article :is(.asidenote::before, .asidenote + sup::before) {
content: counter(asidenotes);
}
}
So far, I’ve set a named anchor on the <main> element (which has an id of thoughts) that encloses a page’s content, reset a counter on each <article>, and inserted that counter as the ::before content for both the asidenotes’ <span>s and the <sup>s that follow them. That done, it’s time to actually position the asidenotes:
#thoughts article .asidenote {
counter-increment: asidenotes;
position: absolute;
anchor-name: --sidenote;
top: max(calc(anchor(--sidenote bottom, 0px) + 0.67em), anchor(top));
bottom: auto;
left: calc(anchor(--main right) + 4em);
max-width: 23em;
margin-block: 0.15em 0;
text-wrap: balance;
text-indent: 0;
}
Here, each class="asidenote" element increments the asidenotes counter by one, and then the asidenote is absolutely positioned so its top is placed at the larger value of two-thirds of an em below the bottom of the previous asidenote, if any; or else the top of its implicit anchor, which, because I didn’t set an explicit named anchor for it in this case, seems to be the place it would have occupied in the normal flow of the text. This latter bit is long-standing behavior in absolute positioning of inline elements, so it makes sense. I’m just not sure it fully conforms to the specification, though it’s particularly hard for me to tell in this case.
Moving on! The left edge of the asidenote is set 4em to the right of the right edge of --main and then some formatting stuff is done to keep it balanced and nicely sized for its context. Some of you will already have seen what’s going to happen here.
Yep, the parentheses came right along with the text, and in general the whole thing looks a little odd. I could certainly argue that these are acceptable design choices, but it’s not what I want to see. I want the parentheses to go away when laid out as a asidenote, and also capitalize the first letter if it isn’t already, plus close out the text with a full stop.
And this is where the whole thing tipped over into “I don’t love this” territory. I can certainly add bits of text before and after an element’s content with pseudo-elements, but I can’t subtract bits of text (not without JavaScript, anyway). The best I can do is suppress their display, but for that, I need structure. So I went this route with the markup and CSS:
<span class="asidenote"><span>(</span>by which I mean a utility role, not an ARIA role<span>)</span></span><sup></sup>
#thoughts article .asidenote span:is(:first-child, :last-child) {
display: none;
}
I could have used shorter elements like <b> or <i>, and then styled them to look normal, but nah. I don’t love the clutter, but <span> makes more sense here.
With those parentheses gone, I can uppercase the the first visible letter and full-stop the end of each asidenote like so:
#thoughts article .asidenote::first-letter {
text-transform: uppercase;
}
#thoughts article .asidenote::after {
content: ".";
}
Then I do a little styling of the asidenote’s marker:
#thoughts article .asidenote::before {
content: counter(asidenotes);
position: absolute;
top: -0.4em;
right: calc(100% + 0.25em);
}
} /* closes out the @supports block */
…and that’s more or less it (okay, yes, there are a few other tweaks to the markers and their sizes and line heights and asidenote text size and blah blah blah, but let’s not clutter up the main points by slogging through all that). With that, I get little asides that are parenthetical in the base text, albeit with a bunch of invisible-to-the-user markup clutter, that will be progressively enhanced into full asidenotes where able.
There’s an extra usage trap here, as well: if I always generate a full stop at the end, it means I should never end my asidenotes with a question mark, exclamation point, interrobang, or other sentence-ending character. But those are things I like to do!
So, will I use this on meyerweb? Heck to the no. The markup clutter is much more annoying than the benefit, it fumbles on some pretty basic use cases, and I don’t really want to go to the lengths of creating weird bespoke text macros — or worse, try to fork and extend a local Markdown parser to add some weird bespoke text pattern — just to make this work. If CSS had a character selector that let me turn off the parentheses without needing the extras <span>s, and some kind of outside-the-element generated content, then maybe yes. Otherwise, no, this is not how I’d do it, at least outside this post. At the very least, some JavaScript is needed to remove bits of text and decide whether to append the full stop.
Given that JS is needed, how would I do it? With custom elements and the Light DOM, which I’ll document in the next post. Stay tuned!