Tab widget

I wanted to learn how to make an accessible tab widget. This will hopefully improve my understanding of ARIA roles, states and properties, as well as helping me to learn some more JavaScript.

Motorhome plans for 2025

Somerset

We wanted to visit Somerset, mainly to see where Liggy was raised and to allow her an opportunity to visit her puppy parents. Whilst we are in the area though, we will take the chance to visit some of the historic towns and cities and a couple of places I've had connections with.

Somerset Plans
Day Plan
Saturday Caravan, camping and motorhome show at the NEC
Sunday Wells: the Bishop's Palace, Evensong at the cathedral, look around the city
Monday Haynes Motor Museum and afternoon tea at Thorner's Farm Shop
Tuesday Bath: the Roman Baths, Guildhall Market, and the Assembly Rooms
Wednesday Clarks Village and Glastonbury Abbey
Thursday Visiting Liggy's puppy parents in Taunton
Friday Lunch with family in Worle
Saturday Travel home, with a stop-off in Stratford-upon-Avon

ARIA learning

Tablist

The first parts of the code define the whole interaction, i.e. a tab widget. The code for this is as follows:

<h2 id="plans">Motorhome plans for 2025</h2>:
<div class="tabs">
<div role="tablist" aria-labelledby="plans">

The div which has the role="tablist" must also have either an aria-label or an aria-labelledby attribute, to ensure that screen reader users know what this tab widget is all about. Because I had already given the section a heading, I used aria-labelledby="plans" and linked it to the id="plans" attribute on the heading.

Tab

The next section of code defines the tabs or the buttons which will be selected to activate each tab panel. The code for these is as follows:

<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabindex="0">Somerset</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2 tabindex="-1">Shetland</button>
<button role="tab"  aria-selected="false" aria-controls="panel-3" id="tab-3 tabindex="-1">Wales</button>
<button role="tab" aria-selected="false" aria-controls="panel-4" id="tab-4 tabindex="-1">The Netherlands</button>

Each of the tab buttons has role="tab" and the attributes aria-selected="true" or aria-selected="false" and an aria-controls which links to the id attribute on each tab panel.

Tabpanel

The final section of code defines the tab panels, which are where the content is displayed for each tab. The code for these follows this example:

<div role="tabpanel" id="panel-1" tabindex="0" aria-labelledby="tab-1">

Each div has role="tabpanel" so that assistive software knows it represents the content. The id="panel-1" links to the aria-controls attribute on the tab button. The aria-labelledby attribute links to the id on each button, so that screen reader users can identify which tab the content relates to.

JavaScript learning

At this stage in my studies, I haven't really got to the detail of JavaScript yet, but making custom widgets does need some knowledge of it... or at least the ability to find and steal borrow some code.

Keyboard navigation for the tabs

The tabs were created using the <button> element. As this is a semantic HTML element, it comes with native keyboard accessibility. There are four buttons, so you would be able to tab from one button to the other. However, this is a tab widget and the expected behaviour would be to tab into the widget and then use the left and right arrow keys to navigate between the four buttons. This requires some JavaScript.

First, we need to define a couple of constants that we can use in our code. These will be related to elements with the roles of tablist (the whole thing) and tab (the buttons).

window.addEventListener("DOMContentLoaded", () => {
  
  const tabList = document.querySelector('[role="tablist"]');
  const tabs = tabList.querySelectorAll(':scope > [role="tab"]');

Next, we need to add a click event handler to each tab. This enables users to click on each tab with a mouse, and because it is on a native HTML element, it also gives some keyboard functionality.

 tabs.forEach((tab) => {
    tab.addEventListener("click", changeTabs);
  });

Then we need to add some magic to enable keyboard users to navigate left and right using the arrow keys. Whichever tab is in focus needs to have its tabindex="0" and the others that are not in focus need tabindex="-1".

  let tabFocus = 0;

  tabList.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
      tabs[tabFocus].setAttribute("tabindex", -1);
      if (e.key === "ArrowRight") {
        tabFocus++;
        if (tabFocus >= tabs.length) {
          tabFocus = 0;
        }
      } else if (e.key === "ArrowLeft") {
        tabFocus--;
        if (tabFocus < 0) {
          tabFocus = tabs.length - 1;
        }
      }

      tabs[tabFocus].setAttribute("tabindex", 0);
      tabs[tabFocus].focus();
    }
  });
});

Showing and hiding tab content

Again, we begin by defining some constants. I will freely admit that this is beyond my coding knowledge and I think is where the exam content stresses that you have to understand the impact of JavaScript on accessibility rather than being able to write your own code.

function changeTabs(e) {
  const targetTab = e.target;
  const tabList = targetTab.parentNode;
  const tabGroup = tabList.parentNode;

Now, we can show the selected panel and hide the rest.

  tabGroup
    .querySelectorAll(':scope > [role="tabpanel"]')
    .forEach((p) => p.setAttribute("hidden", true));

  tabGroup
    .querySelector(`#${targetTab.getAttribute("aria-controls")}`)
    .removeAttribute("hidden");
}

Changing ARIA attributes

This bit actually came before the showing and hiding code. This code sets the aria-selected attribute, so that screen reader users know which tab is currently selected.

  tabList
    .querySelectorAll(':scope > [aria-selected="true"]')
    .forEach((t) => t.setAttribute("aria-selected", false));

  targetTab.setAttribute("aria-selected", true);

Final thoughts

As is often the case for me, I have got all the way to the end, and then realised that I haven't even thought about mobile devices and reflow. So now, I need to go and play with my CSS for small screens and decide how to make that tab widget play nicely for mobile users too.