Cristian Diaz Cristian is a frontend developer from Colombia. He enjoys writing about what he learns and is focusing his career on helping to make web content more accessible to everyone. His main areas of expertise are HTML, CSS, and web accessibility.

Building accessible user interface tabs in JavaScript

19 min read 5430

JavaScript Logo

Interface tabs are a common website component, but developers often face challenges in making this pattern accessible. Screen reader users may encounter some issues reading the content, and users who rely on a keyboard to navigate through a site may require some assistance to make navigation easier.

This article will cover everything you need to know to create an accessible tab interface, including building the HTML structure, adding the CSS styling, and adding JavaScript functionality enhancements.

Here’s the tabbed interface that we’ll build in this tutorial:

Finished Tabbed Interface
A tabbed interface listing different countries. On the left, the tabs are displayed vertically and on the right side, there’s some dummy text and an image for the selected country (in this case, Mongolia).

Let’s get started!

Jump ahead:

Establishing the HTML structure

The HTML structure is very important because it provides screen reader users with the context needed to navigate the interface.

There are two ways that we can use the HTML structure to help define which elements can be focused on with a keyboard. We can use a link-based approach with the <a> element as our tab trigger or we can use the <button> element.

Both approaches take keyboard navigation into consideration by being natively keyboard focusable, but each has advantages and disadvantages. We’ll cover both approaches in this article.

Using the <a> element as the tab

The main advantage of this approach is that we can use the <a> element’s functionality to create the tab’s navigation effect using only HTML and CSS. The <a> element brings keyboard support by taking care of a lot of the focus management and navigation for screen reader users for us!

This approach is also helpful for users who have JavaScript disabled. We’ll only use JavaScript at the end of this tutorial — to enhance some of the <a> element’s functionalities for accessibility purposes.

However, a minor disadvantage to using the <a> element as the tab is that it is counted in the user’s navigation history. In other words, if the user visits all the tabs and then wants to return to their previous site, they’ll have to press the back button many times.

Still, this approach does have progressive enhancement in mind as it offers the greatest tab interface functionality with the lowest technology possible. This approach will work even if JavaScript doesn’t load and even if CSS fails to load as well, so it’s worth considering.

Using the <button> element as the tab

Using the <button> element as the tab has the advantage of not affecting navigation history, making it easier for the user to return to their previous site.



However, this approach requires some extra considerations with HTML to hide all non-active tab content. It also requires some additional decisions with regard to JavaScript, such as determining how to display content, focus management strategies, and some accessibility enhancements.

If you use this approach, you’ll need to consider what will happen if JavaScript doesn’t load on your site. Without a fallback, users will not be able to navigate your tabbed interface. There are several reasons why JavaScript may fail to load. For example, users may have JavaScript disabled due to tracking and privacy concerns, or JavaScript may fail to load due to external factors.

Creating the interface tab structure

After deciding which approach you’ll use, it’s time to start creating the HTML structure!

Tab interface structure with the <a> element approach

Let’s start with the individual tabs. Here, we’ll use the <a> element approach for the HTML structure:

<!-- Tab semantics -->
<ul role="tablist">
  <li role="presentation">
    <a role="tab" href="#panel1" id="tab1">Tab one</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#panel2" id="tab2">Tab two</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#panel3" id="tab3">Tab three</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#panel4" id="tab4">Tab four</a>
  </li>
</ul>

<!-- Tab content semantics -->
<div class="tabpanel-container">
  <section role="tabpanel" id="panel1" aria-labelledby="tab1" tabindex="0"></section>
  <section role="tabpanel" id="panel2" aria-labelledby="tab2" tabindex="0"></section>
  <section role="tabpanel" id="panel3" aria-labelledby="tab3" tabindex="0"></section>
  <section role="tabpanel" id="panel4" aria-labelledby="tab4" tabindex="0"></section>
</div>

Now, let’s examine the structure shown in the above code:

  • At the end of the day, a tab list is just a list of links (or buttons) that will lead us to our content, so the correct semantic for the tab list is to wrap it with our selected element ( <a> or <button>) within the list items inside a <ul> element
  • We add the tablist role to our <ul> to provide more context for screen reader users; this role is used to mark a container that wraps a set of tabs. Since the tablist role does not give the tab semantic to the child element, we add the tab role to our <a> element
  • If a screen reader user fails to read the tablist and tab roles, the HTML will still be read as a list of links or buttons, which is an acceptable fallback
  • We add the presentation role to the <li> elements just to remove the semantics; this will help prevent any weird interactions, while still maintaining our fallback in case a particular screen reader doesn’t support ARIA roles
  • Our tab content will be represented by a <section> element with the tabpanel role. This element is expected to use the same name of the tab as an accessible name. This is why we added the id attribute to our tabs and why we’re using it as a label in our tab panels with the aria-labelledby attribute
  • We add the attribute tabindex="0" to our tab panels to allow items inside the tab (like form fields, links, or buttons) to receive the keyboard focus. This will make it easier for keyboard users to access the content

Tab interface structure with the <button> element approach

If we use the <button>-based approach, we’ll need to include an extra step. We’ll need to add the hidden attribute to all tab panels except the first tab. This attribute will hide the elements for both sighted users and screen reader users. To ensure the first tab remains visible, it should have the aria-selected="true" attribute.

Our markup for the <button> approach should look something like this:

<!-- Tab semantics -->
<ul role="tablist">
  <li role="presentation">
    <button role="tab" href="#panel1" id="tab1" aria-selected="true">Tab one</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel2" id="tab2">Tab two</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel3" id="tab3">Tab three</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel4" id="tab4">Tab four</button>
  </li>
</ul>

<!-- Tab content semantics -->
<div class="tabpanel-container">
  <section role="tabpanel" id="panel1" aria-labelledby="tab1" tabindex="0"></section>
  <section role="tabpanel" id="panel2" aria-labelledby="tab2" tabindex="0" hidden></section>
  <section role="tabpanel" id="panel3" aria-labelledby="tab3" tabindex="0" hidden></section>
  <section role="tabpanel" id="panel4" aria-labelledby="tab4" tabindex="0" hidden></section>
</div>

Additional HTML structure considerations

So far, we’ve covered the initial considerations for our markup, but there are additional factors we’ll need to keep in mind for certain use cases. Some factors will be controlled more dynamically, but we’ll touch on those a little later in this article’s JavaScript section.

Relating tabs with the tab panel

We can relate tabs with the tab panel for screen reader users using the attribute aria-controls. However, this approach will only work on the JAWS screen reader and its use may feel quite verbose, as explained in the article by Heydon Pickering. Instead, we’ll use some focus management strategies which will help keyboard users as well.

Handling vertical tabs

To display a tab list with a vertical orientation, you’ll need to add the attribute [aria-orientation="vertical"] to the container. This will indicate to screen reader users that the tabs are stacked vertically.

This will be important when we start discussing navigation strategies for our sample tabbed interface.

Here’s an example showing how the [aria-orientation="vertical"] attribute would be used in a code snippet:

<ul role="tablist" aria-orientation="vertical">
  <li role="presentation">
    <button role="tab" href="#panel1" id="tab1" aria-selected="true">Tab one</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel2" id="tab2">Tab two</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel3" id="tab3">Tab three</button>
  </li>
  <li role="presentation">
    <button role="tab" href="#panel4" id="tab4">Tab four</button>
  </li>
</ul>

Indicating tab selection

When a user clicks a tab, we’ll need a way to indicate that it was selected.


More great articles from LogRocket:


We’ll use the attribute [aria-selected="true"] to indicate tab selection to screen reader users. Then, we’ll style it with CSS to help indicate selection for sighted users. The [aria-selected="true"] attribute will change dynamically with JavaScript, so it won’t be added to our initial markup.

<li role="presentation">
  <button role="tab" href="#panel1" id="tab1" aria-selected="true">Tab one</button>
</li>

Now that we have a solid HTML structure, it’s time to add some styling with CSS!

Adding styling and layout with CSS

For our example tabbed interface, we’re using a link-based approach with the <a> element as our tab trigger. To style the HTML tab structure, we’ll need to add the tab navigation, create the focus indicator, and identify the selected state’s style.

Adding the tab navigation

To add the tab navigation, let’s add a CSS rule using the :target selector like so:

.tabpanel:not(:target):not(.visible) {
  display: none;
}

:target is a pseudo-class that checks if an element with an id matches the URL’s fragment.

Since we’re using the <a> element here to add functionality, we can use this selector to hide any non-active tab panels. We’re using the visible class as an exception in our example so that we can add some enhancements with JavaScript later.

Creating the focus indicator

Another thing we need to keep in mind is keyboard navigation, and one of the most important things to do in that regard is to create an adequate focus indicator.

You’ve probably noticed a focus indicator while navigating a site using the tab key. Depending on your browser, will appear as a blue or black outline highlighting a selected element.

Browsers offer default focus styles, but they are not sufficient to meet WCAG 2.2 criteria. Therefore, it’s necessary to use additional styling. For more in-depth information on how to create a good focus indicator, read Sara Souiedan’s article on this topic.

For our example, we’ll use the following focus styles:

.tab:focus-visible {
  background-color: royalblue;
  color: whitesmoke;
  outline: 0.2em solid transparent;
}

I decided to use :focus-visible instead of the :focus pseudo-class in order to have the focus style activate only with keyboard navigation. The :focus pseudo-class will also activate when the element is clicked, which can be confusing for users.

:focus-visible is very well supported in modern browsers, so it should not create any kind of conflict. For more information about this property, refer to the MDN documentation.

The “Mongolia” option selected below shows how our tab’s focus state would look:

Mongolia Tab
The first tab in the project tab list has a blue background, providing a visual clue for tab selection to keyboard users.

For this project, I decided to use a transparent outline. This is important for Windows high contrast mode. Under this mode, all text and background colors are replaced with those chosen by the operating system, so we can’t depend on the background color to indicate a focus state. The outline is the only reliable way to indicate the focus state.

Identifying the selected state’s style

Now, let’s come back to what I mentioned earlier regarding using the aria-selected="true" attribute to help screen reader users more easily identify the selected tab. We can use this selector to provide a visual cue for the selected tab as well!

.tab[aria-selected="true"] {
  background-color: var(--component-bg);
}

This approach creates a minor problem because of how CSS specificity works. Both selectors, .tab:focus-visible and .tab[aria-selected="true"], have the same specificity level, and both change the background-color property, so the selector rule order is critical.

We want the focus state’s background-color to overwrite any other background-color, including that of the selected state, so we’ll add the .tab:focus-visible selector rule after the .tab[aria-selected="true"] selector rule.

Here’s our result; notice how both styles interact with each other without any specificity problem:

Myanmar Tab
The first tab in the project tab list is activated and has a gray background, while the third tab is being keyboard focused and has a blue background.

If you’re using the <a> element as your tab, you’ve already created a functional tab interface with the lowest technology possible!

If you used the <button>-based approach, don’t worry! Next, we’re going to add some functionality with JavaScript, which will be crucial for making the tabbed interface more accessible.

Adding enhancements and functionality with JavaScript

There is a lot to unpack in this section; let’s start by checking out what the ARIA Authoring Practices Guide (APG) has to say about this component pattern. Here’s what the ARIA APG’s pattern guide for tab interface considers important for the tab key, the tabindex in tabpanel elements, and the arrow keys.

Managing the tab key

Regarding the use of the tab key, the ARIA APG suggests the following:

When focus moves into the tab list, place focus on the active tab element.

Our first task is to remove the keyboard navigation with the tab key to the non-selected tab so that when the key is pressed, the focus goes directly into the active tab panel. Then, when shift + tab is pressed from the active panel, the focus goes back to its respective tab. Also, we’ll have to indicate tab selection for both screen reader users and sighted users.

Here’s the code I used to address these tasks:

const TABLIST = document.querySelector("#tablist");
const TABS = [...TABLIST.querySelectorAll(".tab")];

const setSelectedTab = (element) => {
  const selectedId = element.id;

  TABS.forEach((e) => {
    const id = e.getAttribute("id");
    if (id === selectedId) {
      e.removeAttribute("tabindex");
      e.setAttribute("aria-selected", "true");
    } else {
      e.setAttribute("tabindex", "-1");
      e.setAttribute("aria-selected", "false");
    }
  });
};

Here, we select all tabs and store the one that is being clicked. Then, we parse the array with all our tabs and compare each id attribute to check if it’s the tab that was selected.

If the tab is not the one we wish to select, we add the tabindex="-1" and aria-selected="false" attributes. However, if the tab is the one we wish to select, we remove the tabindex attribute and add the aria-selected="true" attribute. The <a> and <button> elements are keyboard focusable by default, so there’s no need to add a tabindex="0".

The tabindex="-1" attribute will make an element not focusable with the tab key. When the key is pressed, it will move the focus directly to our active tab panel, enabling us to jump all our tabs. This is also important because it’ll also allow us to manage the focus of the tabs in another that I’ll demonstrate a little later in this article.

Now we all need to do is add event listeners to our tabs to execute this function! For a link-based approach, we’ll need to add an additional event listener to those elements.

With a <button>-based approach, any click event will be activated by the enter and space keys. However, an <a> element will only add those events with the enter key, so we’ll need to add a keydown event listener to check when the space key is pressed.

const handleClick = () => {
  TABS.forEach((element) => {
    element.addEventListener("click", function () {
      setSelectedTab(element);
    });
  });

  // Activates tab with Space. Only necessary if you use the <a> element as a tab

  TABS.forEach((element) => {
    element.addEventListener("keydown", function (e) {
      if ((e.keyCode || e.which) === 32) {
        setSelectedTab(element);
        element.click();
      }
    });
  });
};

Here’s our result!

Vertical Tab Navigation
Sample project with vertical tab navigation. When the tab key is pressed, it moves the keyboard focus to the tab panel; when shift+tab is pressed, it moves the focus to the selected tab.

Managing the tabindex in tabpanel elements

The ARIA APG recommends the following for the tab list keyboard interaction:

When the tab list contains the focus, [tab] moves focus to the next element in the page tab sequence outside the tablist, which is the tabpanel unless the first element containing meaningful content inside the tabpanel is focusable.

Based on this recommendation, our next task is to check if each tab panel contains a focusable element.

For tab panels that do not contain a focusable element, we’ll retain the tabindex="0" attribute we added previously. Otherwise, we’ll update the attribute to tabindex="-1", so when the tab key is pressed, the focus will be moved to the first focusable element inside the tab panel.

We’ll use this code snippet to check if every tab panel contains focusable elements and change the tabindex attribute as needed:

const TABPANELS = [...document.querySelectorAll(".tabpanel")];

const determineTabindex = () => {
  TABPANELS.forEach((element) => {
    const focusableElements = element.querySelectorAll(
      'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)'
    ).length;

    focusableElements
      ? element.setAttribute("tabindex", "-1")
      : element.setAttribute("tabindex", "0");
  });
};

Here’s our result!

Keyboard Navigation
Tabbed interface with keyboard navigation. When a tab panel has a focusable element and we navigate using the tab key, the focus is moved to the first focusable element. Otherwise, the focus will be on the tab panel itself.

In this example, when we try to use our keyboard to navigate the interface, the focused element will vary depending on whether the tab panel contains a keyboard-focusable element.

Managing navigation with the arrow keys

The ARIA APG offers specific keyboard navigation recommendations depending on the orientation of the interface’s tabs.

Here are the recommendations when focus is on a tab element in a horizontal tab list:

left arrow: Moves focus to the previous tab. If focus is on the first tab, move focus to the last tab. Optionally, activates the newly focused tab.
right arrow: Moves focus to the next tab. If focus is on the last tab element, move focus to the first tab. Optionally, activates the newly focused tab.

If you’re building an interface with tabs displayed vertically, besides using the attribute aria-orientation="vertical" you’ll have to use the up arrow and down arrow keys instead of the left arrow and right arrow keys.

Here are the recommendations when a tab list has aria-orientation set to vertical:

When a tab list has its aria-orientation set to vertical:
down arrow performs as right arrow is described above.
up arrow performs as left arrow is described above.

My intention for this project was to build a completely flexible interface, but with this approach at some point the screen will end up being either a horizontal tab list or a vertical tab list. How do we address this?

Let’s start by determining what arrow keys we should use. In this particular case, we have two alternatives:

  1. Check the tab list width and height. If the height is greater than the width, add the attribute aria-orientation="vertical" and support only up arrow and down arrow navigation. Otherwise, add the attribute aria-orientation="horizontal" and support only left arrow and right arrow navigation.
  2. Support all four navigation keys at the same time. If the user presses the down arrow or right arrow, it will move the focus to the next tab. If they press the left arrow or up arrow, it will move the focus to the previous tab.

I usually prefer the simplest solution, so I’ll choose the second option. However, each project is different. If your tab list will always be in a vertical position, you‘d be better off using aria-orientation="vertical" and supporting only up arrow and down arrow navigation.

Here’s the code I used, based on the second option:

const createArrowNavigation = () => {
  const firstTab = TABS[0];
  const lastTab = TABS[TABS.length - 1];

  TABS.forEach((element) => {
    element.addEventListener("keydown", function (e) {
      if ((e.keyCode || e.which) === 38 || (e.keyCode || e.which) === 37) {
        if (element == firstTab) {
          e.preventDefault();
          lastTab.focus();
        } else {
          e.preventDefault();
          const focusableElement = TABS.indexOf(element) - 1;
          TABS[focusableElement].focus();
        }
      } else if (
        (e.keyCode || e.which) === 40 ||
        (e.keyCode || e.which) === 39
      ) {
        if (element == lastTab) {
          e.preventDefault();
          firstTab.focus();
        } else {
          e.preventDefault();
          const focusableElement = TABS.indexOf(element) + 1;
          TABS[focusableElement].focus();
        }
      }
    });
  });
};

Previously, we created a variable that stores all our tabs. We can use it to determine which is the first and last navigation tab. This is important because these tabs have special behavior.

Next, we can check which key codes are related to each arrow key. For that, we can use the site keycode.info to check it out. To ease the process, here’s the list of the key codes:

Key Keycode
Left Arrow 37
Up Arrow 38
Right Arrow 39
Down Arrow 40

Now, we listen to each key to check the position of the focused tab in the array and we focus on the next element (the right arrow and down arrow) or previous element (the left arrow and up arrow) in the array with the focus() method. Since these elements have the tabindex="-1" attribute, we’ll be able to focus them with this method.

Keep in mind that if the element is the first one on the array, it will move the focus to the last element and vice versa. Another point to consider is that the arrow keys also have the function of moving the scrollbar; we can prevent this behavior with the e.preventDefault() method.

Here’s our interface, showing the use of arrow keys to move between tabs:

Using Arrow Keys
Tabbed interface with keyboard navigation, using arrow keys to move between tabs.

Determining how to show the content

In addition to managing the focused state of our component, we need to determine the timing for activating the newly focused tab. In other words, when should the tab panel be shown? Should it be shown when the tab is clicked or when focus is brought to the selected tab?

The answer to this topic is surprisingly nuanced, and W3C has an entire section regarding this topic. To summarize W3C, changing the displayed content at the moment of focusing an element (referred to as follow focus) can be beneficial for certain components but creates accessibility errors for others.

Displaying content at time of click

Displaying content at the time of focus can make keyboard navigation easier for sighted users, but screen reader users may not be aware that new content has been added to the page. Additionally, we need to consider the amount of content that will be shown as this could impact performance.

I suggest displaying the content when the tab is clicked. This option could require an extra key press for keyboard users, but it will provide a more inclusive experience for screen reader users.

When the <a> element is clicked, the focus is brought to the selected tab panel, which will cover navigation for screen reader users.

Displaying content at time of focus

If you decide to display the content based on focus, be sure to use the <button> approach, rather than the <a> element approach.

With a <button> element, you’ll need to handle the focus management with JavaScript. For that, we’ll come back to the handleClick function we created earlier in the CSS section of this article, and we’ll add some adjustments.

First, we’ll create a function that adds or removes the attribute hidden depending on whether the tab panel is being targeted. If the tab panel is the targeted item, we’ll also move the keyboard focus to it. Here’s my approach to this issue:

const showActivePanel = (element) => {
  const selectedId = element.id;
  TABPANELS.forEach((e) => {
    e.hidden = "true";
  });
  const activePanel = document.querySelector(
    `[aria-labelledby="${selectedId}"]`
  );
  activePanel.removeAttribute("hidden");
        activePanel.focus()
};

What we‘re doing here is hiding all tab panels by adding to them the hidden attribute. Next, we’ll select our targeted tab panel by using the aria-labelledby attribute’s value. Remember, each button has an id that we’re using to label the tab panel by using this attribute; this will help us select the correct tab panel.

Now, we just remove the hidden attribute and then bring the keyboard focus to it with the focus() method. Finally, we add the showActivePanel function in our handleClick function to be executed on the click.

Now it’s time to see how the component works as a whole!

Click Navigation
Tabbed interface showing content on the click; arrow keys are used for keyboard navigation between tabs.

Handling the <a> element’s initial state

If we opt to use an <a> element for our tabs, we’ll need to handle the element’s initial state if JavaScript is active.

Here are the actions we want to take when the document loads:

  • If the site’s URL is not pointing to the tab panels, the first panel should be visible, the first tab should have the attribute aria-selected="true" and be keyboard focusable when the tab key is pressed; the other tabs should have aria-selected="false" and tabindex="-1" attributes
  • If the site’s URL is pointing to a panel, the panel in this URL should be visible; the respective tab should have the previously mentioned attributes: aria-selected="true" and no tabindex

We’ll create the functions for both scenarios.

const activateFirstPanel = () => {
  TABS[0].setAttribute("tabindex", "0");
  TABS[0].setAttribute("aria-selected", "true");
  TABPANELS[0].classList.add("visible");
};

This function will cover our first case. It’s actually quite simple. We have the TABS and TABPANELS node lists already, we can select the first element of each list and add the required properties.

In the earlier CSS section, I mentioned that we were using the visible class as an exception and would add some enhancements with JavaScript later. Well, now it’s the moment for the JavaScript enhancements!

We add this class to the first tab panel to make it visible. Once the user starts interacting with the tab, we’ll need to remove this class.

const checkInitialSelectedTab = () => {
  const targetedTabPanel = document
    .querySelector(".tabpanel:target")
    .getAttribute("aria-labelledby");
  const selectedTab = document.querySelector(`#${targetedTabPanel}`);
  selectedTab.setAttribute("aria-selected", "true");
  selectedTab.removeAttribute("tabindex");
};

Our second scenario is a bit more complicated. Since this panel is being targeted, it will be displayed on the screen by default — this is due to the selector
.tabpanel:not(:target):not(.visible) we used previously. But, we need to look for the tab that activates the panel.

For this, we’ll select the targeted tab panel with JavaScript by using the .tabpanel:target selector in the querySelector() method. Once we have that node, we’ll get the aria-labelledby attribute. The tab’s id is the same as the one that we’re using in this panel’s aria-labelledby attribute, so we’ll use that id to look for the tab, and then we’ll add the necessary attributes.

Now, we just need to execute those functions depending on what the URL contains, so we’ll use another function to handle this:

const handleInitialState = () => {
  TABS.forEach((e) => {
    e.setAttribute("tabindex", "-1");
    e.setAttribute("aria-selected", "false");
  });

  window.location.href.indexOf("#panel") === -1
    ? activateFirstPanel()
    : checkInitialSelectedTab();

  determineTabindex();
};

Well, I lied. There is just a little bit more work that we need to do in this function.

For starters, let’s set the aria-selected attribute of all tabs to false and the tabindex attribute to -1; then we’ll fix the correct one with the created functions.

Next, we need to check if a tab panel is being targeted in the site’s URL. We can use the window.location.href method to get the URL and then use the array indexOf method to check if there is a panel targeted in this URL.

When you use an array function in a string, such as our URL, it will create an array with each character in the string as an element of this array, if the whole world is in this URL, it’ll return the starting position of the string, if not, it’ll return -1. This is what we’ll use to determine what function to execute.

Now, we’ll execute the determineTabindex() function we previously created. We’ll remove the no-js class that we added in our HTML and used to create a fallback in case JavaScript doesn’t load.

We’re almost done!

We previously created a function to check if the tab panels have focusable elements: determineTabIndex(); that function needs to be added there.

Do you remember our setSelectedTab() function? We need to add a small line to remove the visible class in case it was used. To do so, we’ll add the code TABPANELS[0].classList.remove("visible"); before we start checking each tab.

Finally, let’s use the following command to make sure our function handleInitialState() runs when the site loads:

window.onload = handleInitialState;

Remember, this section only applies if you used the <a> element to handle the tabs’ navigation.

Wrapping up

Below is our completed accessible tabbed interface! After considering multiple options, I used the link-based approach with the <a> element for this demo. The tabbed UI was designed with a completely flexible layout with no need for media queries.

See the Pen
Accessible tabbed interface
by Cristian Diaz (@ItsCrisDiaz)
on CodePen.

This component addresses the main accessibility issues that developers encounter when building a tabbed interface: ensuring that it works properly for keyboard users and that the information is shown correctly to screen reader users.

Building an accessible component can be challenging due to the multiple nuances of accessibility requirements, but it’s important to ensure your website is usable for everyone.

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Cristian Diaz Cristian is a frontend developer from Colombia. He enjoys writing about what he learns and is focusing his career on helping to make web content more accessible to everyone. His main areas of expertise are HTML, CSS, and web accessibility.

One Reply to “Building accessible user interface tabs in JavaScript”

  1. Massive thanks for this tutorial. I just build my forever reusable tab component. 🙂

    (I’m far from your experience, but could’t you unify some of the ‘e.preventDefault’ in the ‘keydown’ event listeners?)

Leave a Reply