Recently I've found myself needing to insert endnotes and/or links to references when writing articles for this blog, if I want to cover some incidental detail of a topic under discussion without distracting from the main piece. I've used the word "endnote" here as the end of the post is a good place to put these asides, collated in one place out of the way; however, this can cause an issue for the diligent reader who'd like to look over an endnote at the time it's referenced. Having scrolled (or having been linked) to the bottom of the page, it can be problematic to come back up to where one was and continue with the main article.
To that end, I've instead started using inline tooltips for asides, which appear when the referential link is hovered over by the mouse (or tapped by the mobile user); this allows the aside to enrich the article without distracting from its flow.
We can get almost all the way to a tooltip like this with CSS alone; let's have a look at how that works.
CSS for tooltip positioning
The most salient piece of CSS being used for these tooltips is the position
rule: if an element on the page is positioned with the absolute
value, it will anchor itself based on an offset from the nearest positioning context. That context can be the whole page, but more often it's a parent element that's been given the relative
position value; the nearest relatively positioned parent to an absolutely positioned element will provide that context.
HTML for a positioned tooltip
<p>We discuss a topic, and then provide a reference <cite> <sup>[1]</sup> <mark><em>The Lord of the Rings</em> (Harper/Collins), pg 293</mark> </cite> which can make for further reading.</p>
CSS for the tooltip
cite { position: relative; } cite sup { cursor: pointer; } cite mark { display: none; position: absolute; top: 0.5em; left: 0; } cite:hover mark { display: block; }
Display patches
This gets us most of the way to a working tooltip already, but there are a few things we can do to help with the display:
- Background: As it stands, the tooltip
mark
is transparent, so the text of the tooltip renders over the top of the article and is hard to make out. This can be alleviated by giving themark
abackground
colour, and acolor
so the text can sit atop the background and be read well. - Width: Without a defined stipulation on width, the tooltip will only be as wide as the widest non-wrapping piece of content inside (generally, this would be the single longest word). We can make the tooltip more readable by giving it a minimum width based on the size of the viewport (the window being used to view the page); for example:
min-width: 30vw
to give the tooltip a width of at least 30% of the viewport, and more if the browser deems it necessary. - Z-index: Figure 1 shows our tooltip showing above text and images on the page. A detail of the implementation of images on this site is that some images (those with black text on a transparent background) are marked invertable so they're readable in dark mode; this adds a
filter: invert(1);
rule when dark mode is enabled at the body level. In turn, this puts the image in a new stacking context[2]Mozilla Developer Network, Stacking context, Feb 2024 which must be overridden by the tooltip with az-index
rule. - Accessibility: The tooltip content inside the
mark
is hidden by CSS unless one hovers on thesup
; screen readers will generally disregard this content as invisible, meaning the additional context given by the aside won't be available in these browsers. This can be ameliorated by providing another CSS rule within a media query for non-screen users, as well as usingvisibility
instead ofdisplay
to control whether the tooltip appears.
Putting these patches together gets us to the tooltips being used on this page, for which the CSS looks as follows:
cite { position: relative; } cite sup { cursor: pointer; } cite mark { visibility: hidden; position: absolute; top: 0.5em; left: 0; z-index: 2;/* Additional rules to add some nicety */color: var(--g-text); background: var(--g-bg); border: 4px solid var(--g-border); border-radius: 4px; padding: 12px; min-width: 30vw; } cite:hover mark { visibility: visible; } @media not screen { cite mark { visibility: visible; } }
Except that's not accessible
For some screenreaders, this may be enough: the visibility
rule puts the content of the tooltip into the page without having it initially visible. For Voiceover on macOS, however, the content needs to be rendered somewhere on the page so it can be read out.
Historically, the canonical way to have something render offscreen on a page without using display
or visibility
has been to position it off either the left or right edge of the screen. This still works, but positioning something off the right of the page is liable to push the scrollbar out, so we use negative left
values to shunt the content off to the left; this places it in the DOM, visible to screenreaders, but not visible until we reset the left
value.
cite mark { position: absolute; top: 0.5em; left: -9999px; } cite:hover mark { left: 0; }
With this positioning in place, Voiceover correctly picks up on the tooltip's presence in the page without having the tooltip render for sighted users, until the sup
is hovered.
Positioning on the right
The only remaining issue with these tooltips is what happens when the sup
trigger for the tooltip lies towards the right of the screen (anywhere past 70% across from the left of the viewport). In this case, bringing up the tooltip causes it to render with its min-width
of 30% of the viewport, causing the right edge to be placed past the right edge of the content, and causing horizontal scroll.
Unfortunately, this is where we have to diverge from a pure-CSS solution, as the following doesn't exist in CSS as it stands:
Hypothetical selector for elements towards the right of the screen
cite[position-right > 70vw] mark { left: auto; right: 0; }
Instead, we must resort to JavaScript which can determine the rendered position of any tooltip triggers on the page, after the page has loaded. We can use the querySelectorAll
method of the document
to query for matching elements, and each matching element has a getBoundingClientRect
method which will provide its position and size on the page.
Once we've determined whether the matching element is far enough to the right, we can add or remove a class
to the element as appropriate. This can be done by using the corresponding methods of the element's classList
.
JavaScript: Tooltip positioner function
window.onload = function() { const tooltipPositioner = () => { const threshold = window.innerWidth * 0.7; document.querySelectorAll('cite').forEach(el => { el.classList[ el.getBoundingClientRect().x > threshold ? 'add' : 'remove' ]('right'); }); }; tooltipPositioner(); };
CSS: Rule for tooltips on the right
cite.right mark { left: auto; right: 0; }
Repositioning after resize
One consideration that needs to be made now that some tooltips can have a different behaviour for "is on the right of the screen" is what happens if the user resizes their browser window, and causes some tooltip triggers to move across the viewport as the content reflows. Fortunately, JavaScript offers the ResizeObserver
which allows for a function to be run whenever an element resizes; as our positioning code is already in a function, setting up the observer against resizes on the document's main
tag is fairly simple:
const tooltipPositioner = () => { ... }; tooltipPositioner(); (new ResizeObserver(tooltipPositioner)).observe( document.querySelector('main') );
It should be noted that the ResizeObserver
runs every time the element changes size: if the user is resizing their browser window, this might fire a hundred times before the size of the window settles. For our purposes, as this is the only JavaScript running on the page, performance isn't a particular concern; if this tooltip code were to be used as part of a heavier framework, one may wish to use throttling mechanisms to ensure that the code is only run every so often while still remaining responsive to resize events.
But we almost got away without using any JS at all. With the new popover
API rapidly rolling out to browsers as of the time of writing, it may soon be the case that even the CSS used here can be trimmed back.