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:

  1. It’s generally a bad idea to add tabindex to non-interactive elements for a few reasons:
    1. 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.
    2. 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.
    3. 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.
    4. While not a popular pairing, IE11 and NVDA would go into forms mode when an <abbr> with a tabindex received focus.
  2. 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.
  3. 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.

blog comments powered by Disqus