Revisiting the abbr element
A few weeks ago, I wrote about how to make the abbr
element work for touchscreen, keyboard, and mouse. My suggestion involved making the <abbr>
element focusable with the tabindex
attribute and using pseudo-elements to create a tooltip from the title
attribute.
<abbr title="Cascading Style Sheets" tabindex="0">CSS</abbr>
abbr[title]:hover::after,
abbr[title]:focus::after {
content: attr(title);
/* more styles to position the elelent like a tooltip */
}
Since then, I’ve gotten some feedback on some accessibility-related issues to the implementation. Scott O’Hara in particular raised a lot of important concerns, which can be summarised in the following points:
- It’s generally a bad idea to add
tabindex
to non-interactive elements for a few reasons:- It’s unexpected behaviour. Especially as there is no associated
role
, users of assistive technology may be left wondering why it is in the focus order since it’s not an interactive element. - If there are many instances of it, it makes navigating the DOM a pain for keyboard users as they have to traverse through each one.
- Having focus move to the
<abbr>
element resulted in some awkward behaviour for users of the NVDA screen reader. With Firefox for instance, the content of the<abbr>
would be would announced on first focus, but if re-navigating to an<abbr>
, NVDA might not announce anything. - While not a popular pairing, IE11 and NVDA would go into forms mode when an
<abbr>
with atabindex
received focus.
- It’s unexpected behaviour. Especially as there is no associated
- There’s no acceptable way to dismiss the tooltip. Particularly on touch devices where the user would have to click on another focusable element. This would fail the new WCAG 1.4.13 rule.
- There can be duplication of content for users of the JAWS screen reader who already have a setting to announce the
title
attribute of<abbr>
elements.
With this feedback, I decided to revisit this and create a more accessible solution.
A quick aside - avoid tooltips! #
Before I go on to how I attempted to fix my original solution, it should be noted that, if all you want to use a tooltip for is to provide the full description of an abbreviation, you’re probably better off just providing it inline.
The first time an abbreviation is used, spell it out entirely.
<p>Cascading Style Sheets (<abbr>CSS</abbr>)</p>
Everywhere else in the document, you can use the <abbr>
element as normal. Since the abbreviation has already been defined earlier in the document, the limitations of the native <abbr>
element are not so harmful.
<p><abbr title="Cascading Style Sheets">CSS</abbr> is awesome.</p>
For most use cases, this is probably the simplest solution and provides an adequate experience for all users.
Enhancing the abbr
element #
All of that said, here’s my attempt at enhancing the <abbr>
element and creating a tooltip experience that works for touchscreen, keyboard, and mouse. This solution isn’t perfect, as I discuss below under the limitations, but I wanted to make an attempt anyway :)
Refactoring the markup #
One thing I gathered from the feedback was that the actual <abbr>
element will never be acceptable to use if we want to create the experience of the tooltip. So, I needed to change the actual markup to look something more like this:
<span data-tooltip>
<a href="#tt" aria-describedby="tt"><abbr>CSS</abbr></a>
<span id="tt" role="tooltip" class="hidden">
Cascading Style Sheets
</span>
</span>
To start with, I changed the element that contains the abbreviation to a <a>
, which links to the tooltip. I used an <a>
element because I needed an element that was actually interactive, instead of applying tabindex
to a non-interactive element.
Next, I made the tooltip itself an element within the HTML (as opposed to a CSS pseudo-element), and added the tooltip
ARIA role so it’s clear what its purpose is. To comply with the tooltip requirements, I added an aria-describedby
attribute that references the tooltip itself.
Handling events #
In order to make the tooltip usable by touchscreen, keyboard, and mouse users, I needed a lot of event listeners.
Event | Action | Input device |
---|---|---|
mouseenter |
Show Tooltip | Mouse |
mouseleave |
Hide Tooltip | Mouse, Touchscreen |
keyup |
Show Tooltip | Keyboard |
keydown |
Hide Tooltip | Keyboard |
click |
Show Tooltip | Touchscreen |
Besides the mouseenter
and mouseleave
events, I could listen for each of these events on the document
and show/hide the tooltip depending on the element that triggered the event. For example, this is what the keyup
event looks like.
document.addEventListener("keyup", (e) => {
if (e.keyCode === 9 && e.target.parentElement.hasAttribute("data-tooltip")) {
showTooltip(e.target.parentElement);
}
});
Showing and hiding the tooltip #
To show or hide the tooltip, I simply toggled the .hidden
class, which sets the display
to none
.
function showTooltip(tt) {
tt.querySelector("[role='tooltip']").classList.remove("hidden");
}
function hideTooltip(tt) {
tt.querySelector("[role='tooltip']").classList.add("hidden");
}
Progressive enhancement and automation #
Because the functionality of the tooltip requires Javascript to work, I wanted a case where the written markup would be a plain <abbr>
element and, only if Javascript is enabled, would the element be changed to the refactored tooltip markup above.
So, I wrote a small script that would take a regular <abbr>
, and change it to the markup required. Here’s what that script looks like -
const abbrs = Array.from(document.querySelectorAll("abbr[data-tooltip]"));
abbrs.forEach((abbr, index) => {
const tooltipContainer = document.createElement("span");
tooltipContainer.setAttribute("data-tooltip", "");
tooltipContainer.innerHTML = `
<a href="#tt-${index}" aria-describedby="tt-${index}">
<abbr>${abbr.innerHTML}</abbr>
</a>
<span role="tooltip" id="tt-${index}" class="hidden">${abbr.getAttribute("title")}</span>
`;
abbr.parentElement.replaceChild(tooltipContainer, abbr);
tooltipContainer.addEventListener("mouseenter", (e) => showTooltip(tooltipContainer));
tooltipContainer.addEventListener("mouseleave", (e) => hideTooltip(tooltipContainer));
});
So, without Javascript enabled, my markup would simply look like this -
<abbr data-tooltip title="Cascading Style Sheets">CSS</abbr>
This is easy to write and maintain. If Javascript is enabled, the markup will be changed to the tooltip.
You can view the full project on CodePen to see how this works.
Limitations #
As I mentioned, this implementation isn’t without it’s limitations.
For example, using the <a>
element here may not be the most semantic, as it is not really intended to be a link. I used the <a>
because I wanted an element that was interactive, not because I wanted anyone to be able to link to the tooltip itself. There doesn't seem to be a widely-accepted implementation of tooltips at the moment, so I just went with what I thought was best given. An alternative to this would be to use a <button>
instead, which will require specifying that it controls another element via the aria-controls
attribute.
On the other hand, the fact that a link is used could be leveraged on to provide more useful information. For example, instead of linking to the tooltip itself, the link could go to a page with more information about the abbreviated word.
Additionally, breaking up the DOM into too many links / interactive elements is still a concern. Even if this tooltip were to be used, it should probably only be used for the very first instance of the abbreviation on the page.