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.
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 |
The Shetland Islands
The Shetland Islands are a bucket list destination for me! Lying many miles north of the British mainland, their remote location and natural beauty have always appealed to me. I imagine there will be some similarities with my beloved Finland in terms of the landscape, though I'm told Shetland has very few trees.
Our plan is to drive to Aberdeen, a good two day journey, and then take the overnight ferry to Lerwick. The ferry calls in at the Orkney Islands in the middle of the night, but I'll hopefully be asleep at that point.
We will begin our road trip by driving to the southern tip of the main island, Sumburgh Head, where I hope to see puffins and other sea birds. There is also a large Viking site there called Jarlshof, which looks to be mostly accessible, so we'll be going there too.
Driving north, we'll be stopping off at various places, including some beautiful beaches. The one I am most looking forward to is St Ninian's beach. This is the UK's largest tombolo and is a scene that was used frequently in the BBC Shetland series. It also comes recommended by Liggy's advanced trainer, who tells us that her demo dog Rio, absolutely loved playing there.
At some point, we'll take intra-island ferries over to Unst, where we hope to visit more Viking attractions and get as close as we can to being as far north as you can go in the UK. Coming back towards Lerwick, we hope to stop on other islands: Yell and Fetlar, both renowned for bird watching but also hoping for an opportunity to see Orca and other sea mammals.
We will end our tour back in Lerwick, where we hope to spend a couple of days. To enable this, we may stay on Bressay, an island just off Lerwick. We will explore the town and hopefully visit some more of Perez and Tosh's regular haunts, as we are definite Shetland fans!
The Welsh Coast
Our previous visits to Wales have largely been to Cardiff, with one trip to Anglesey and Snowdonia. So this year, we are going to drive the Welsh coast, from South to North, visiting many of the towns and cities on the way. We are staying entirely on Caravan and Motorhome Club (CAMH) sites and we will build in plenty of time to rest and relax.
We'll be starting our route in Swansea, staying at Gowerton. Then we head to Tenby, Pembroke, St David's, Cardigan, New Quay, Aberystwyth, Porthmadoc and Caernarfon.
The Netherlands
Somebody told me that the Netherlands is one of the most accessible countries in Europe. I hope so! When I was 14, I went on a school trip to the Netherlands and really enjoyed it. I found it fascinating for several reasons, but mostly to do with cheese, history and science.
Living so close to Hull, the obvious route for us is to take the overnight ferry from Hull to Rotterdam. We will then spend a good day or two in Rotterdam, enjoying the sites. On my previous trip, many years ago, we went up Euromast, and I really want to do that again.
From Rotterdam, we will move to Delft, famous for its pottery. We also intend to visit Madame-de-Berry's hidden house, which was obviously intended for me!
Then we will move onto The Hague, which is the political centre for the country and then to Amsterdam, the actual capital. I haven't planned much yet for days out there but I think we could easily spend two whole days just looking around the city, so we may play it by ear.
Did someone mention cheese? Edam will be our next stop, followed by Gouda, via Volendam. Edam and Gouda are famous for their cheeses and we will no doubt visit the various factories, museums and shops. It would be rude not to taste and buy!
The lovely thing about this holiday is that we will arrive back in Hull on the overnight ferry and have almost no journey home. If the overnight ferries work out for us, we may use this as our main route to the continent for future trips.
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.