I am creating this post as notes from Wes Bos Javascript course which you can sign up for and do with me here: https://beginnerjavascript.com/. Here is a link to Wes' GitHub readme.md.
These are notes that follow along on Slide 36 of the Beginner Javascript course. Follow along for some insights into aria-selected
, aria-label
, how to make tabs work using vanilla Javascript and CSS!
aria-selected
& aria-label
Some HTML to reference at while reading this section:
<div role="tablist" aria-label="Programming Languages"> <button role="tab" aria-selected="true" id="js"> JavaScript </button> <button role="tab" aria-selected="false" id="ruby">Ruby </button> <button role="tab" aria-selected="false" id="php"> PHP </button> </div> </div> <div role="tabpanel" aria-labelledby="js"> <p>JavaScript is great!</p> </div> <div role="tabpanel" aria-labelledby="ruby" hidden> <p>Ruby is great</p> </div> <div role="tabpanel" aria-labelledby="php" hidden> <p>PHP is great!</p> </div>
In the HTML in this project you will see that there are attributes for "buttons" called aria-selected
& aria-label
, these are for semantics and improve readability for search engines so they understand there is content behind the tabs. You establish the tabs inside of a div
with an attribute called role
which should be set to tablist
. Every tab contained within should have a role
of tab
.
aria-selected
can be set to true
or false
, this is the equivalent of form attributes being set to active
and makes one of the tabs open (you should have 1 set to true and the rest false.
aria-label
is just semantical, it doesn't do anything specifically beyond SEO
Totally separate by that, these are connecting the tabpanel
with a tab
from the tablist
by the aria-labelledby
which is set to equal the id
of the individual tabs.
One last thing you will notice is hidden
, this is great because you do not have to use CSS to hide it, it's a built in feature to this HTML element.
Selecting
If you haven't checked out my Dom Manipulation guide on selecting, you should give it a peek.
Let's select our main components and establish our variables:
const tabs = document.querySelector('.tabs'); const tabButtons = tabs.querySelectorAll('[role="tab"]'); const tabPanels = tabs.querySelectorAll('[role="tabpanel"]');
Bear in mind
[role="tab"]
format allows us to search for attributes within an element which is why the tabbutton
is called fromtabs
Event Listener
If you haven't checked out my Javascript Event's article, this section will rely heavily on knowledge built there.
Now we need to listen for each tab button:
tabButtons.forEach(button => button.addEventListener ('click', handleTabClick));
Remember because they are buttons, they will work with the keyboard or with a click by default using the above event listener. Also note we haven't created the handleTabClick() function yet so right now this would error. Event listeners need 2 arguments and typically the second handler
Handler
Let's create a function that will be called handleTabClick
so our Javascript console stops yelling at us.
If you haven't checked out my article on Javscript functions, it's pretty short but understanding functions could also be pretty helpful here.
This will be a pretty basic function to start so we can that it is working.
function handleTabClick(event){ console.log(event); }
It should look something like this:
Structure With "Sudo Code"
Now we will need to create a loop which we haven't gotten into much detail yet on but we will. If you struggle in this section, it's ok - just press the "I believe button" and keep on rolling through this. We are going to start with some sudo code:
function handleTabClick(event){ // HIDE OTHER TAB PANELS // MARK ALL TABS AS UNSELECTED // MARK THE CLICKED TAB AS SELECTED // FIND THE ASSOCIATED TABPANEL AND SHOW IT }
Start by looping through all the tabPanels
we previously selected and console.log them like so:
function handleTabClick(event){ // HIDE OTHER TAB PANELS tabPanels.forEach(function(panel){ console.log(panel); }) // MARK ALL TABS AS UNSELECTED // MARK THE CLICKED TAB AS SELECTED // FIND THE ASSOCIATED TABPANEL AND SHOW IT }
When we click through each of the tabs, we will see something like this:
Hiding All The Tabs
What we need to do next is take the panel
and use it's property hidden
and set it to true.
const tabs = document.querySelector('.tabs'); const tabButtons = tabs.querySelectorAll('[role="tab"]'); const tabPanels = tabs.querySelectorAll('[role="tabpanel"]'); function handleTabClick(event){ // HIDE OTHER TAB PANELS // console.log(tabPanels); tabPanels.forEach(function(panel){ // console.log(panel); panel.hidden = true; }) // MARK ALL TABS AS UNSELECTED // MARK THE CLICKED TAB AS SELECTED // FIND THE ASSOCIATED TABPANEL AND SHOW IT } tabButtons.forEach(button => button.addEventListener ('click', handleTabClick));
This will hide all the tabs, now we want to go back and unhide the one we selected.
Unhide The Selected Tab
We are going to loop through again on the tab
and set the aria-selected
property to false
;
Anytime you see a "dashed" attribute like
aria-selectedyou will not be able to use it like
tabButton.aria-selectedbut in most cases can call it using camel case like this:
tabButton.ariaSelectedand it still works.
It should look something like this:
const tabs = document.querySelector('.tabs'); const tabButtons = tabs.querySelectorAll('[role="tab"]'); const tabPanels = tabs.querySelectorAll('[role="tabpanel"]'); function handleTabClick(event){ // HIDE OTHER TAB PANELS tabPanels.forEach(panel=>{ panel.hidden = true; }) // MARK ALL TABS AS UNSELECTED tabButtons.forEach(tab=>{ // tab.ariaSelected = false; // DOES NOT WORK tab.setAttribute('aria-selected', false); }); // MARK THE CLICKED TAB AS SELECTED // FIND THE ASSOCIATED TABPANEL AND SHOW IT } tabButtons.forEach(button => button.addEventListener ('click', handleTabClick));
You will now see the aria-selected
attribute set to false when clicking on another tab.
Mark The Clicked Tab As Selected
Notice we aren't using any classes here because we are using an accessibility attribute instead! Let's set that click event to turn aria-selected
to true.
event.currentTarget.setAttribute('aria-selected', true);
Now we have an active tab but we don't have the matching tabpanel associated with it. Let's fix that!
Find the Associated tabPanel
And Show It
Let's create a deconstructed property variable of the currentTarget
like this:
// FIND THE ASSOCIATED TABPANEL AND SHOW IT const {id} = event.currentTarget; console.log(id);
We need to find the associated id
for the tab panel. There are a couple of ways we can go about this:
Method 1: Use The id
In A Query Selector
const tabPanel = tabs.querySelector(`[aria-labelledby="${id}"]`); tabPanel.hidden = false;
Since the aria-labelledby
is similar to form name
, you can attach that tabPanel
with it's associated tab
.
What you see above is we select the aria-labelledby
attribute (this is done inside of the [here]
brackets).
We are calling a variable inside of the variable using ${id}
. You ou saw from the previous screenshot that the id matches the tab's id
attribute from the HTML.
Method 2: Use A Find In The Array Of tabPanels
You may need to review my functions article to get a better understanding of the arrow function in this section. Personally, I think this method is much harder to read than the 1st method. Easier to read code > short code!
We could try to use: tabPanels.find();
but the problem is that find()
can only be used on an array
. The tabPanels
is really just a node list. There is a way we can change the variable tabPanels
to an array like this:
// const tabPanels = tabs.querySelectorAll('[role="tabpanel"]'); const tabPanels = Array.from(tabs.querySelectorAll('[role="tabpanel"]'));
What we can do with find()
is pass in a function:
const tabs = document.querySelector('.tabs'); const tabButtons = tabs.querySelectorAll('[role="tab"]'); // const tabPanels = tabs.querySelectorAll('[role="tabpanel"]'); const tabPanels = Array.from(tabs.querySelectorAll('[role="tabpanel"]')); function handleTabClick(event){ // HIDE OTHER TAB PANELS tabPanels.forEach(panel=>{ panel.hidden = true; }) // MARK ALL TABS AS UNSELECTED tabButtons.forEach(tab=>{ tab.setAttribute('aria-selected', false); }); // MARK THE CLICKED TAB AS SELECTED event.currentTarget.setAttribute('aria-selected', true); // FIND THE ASSOCIATED TABPANEL AND SHOW IT const {id} = event.currentTarget; console.log(id); // Method 1: Use The id In A Query Selector // const tabPanel = tabs.querySelector(`[aria-labelledby="${id}"]`); // tabPanel.hidden = false; // Method 2: Use A Find In The Array Of tabPanels const tabPanel = tabPanels.find( panel => panel.getAttribute('aria-labelledby') === id); tabPanel.hidden = false; } tabButtons.forEach(button => button.addEventListener ('click', handleTabClick));
Final project:
See the Pen Tabs by Drew (@DrewLearns) on CodePen.
My Ask and Closing thoughts:
If you found this article helpful, share/retweet it and follow me on twitter @codingwithdrewk! There is so much more in Wes' courses I think you will find valuable as I have. I'm learning so much and really enjoying the course, Wes has an amazingly simple way to explain the difficult bits of Javascript that other courses I've taken could only wish. You can view the course over at WesBos.com. (I am in no way getting any referrals or kickbacks for recommending this)
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.