Skip to main content

The A11y Path

This blog aims to cover: W3C Guidelines, Assistive Technologies (AT), European Accessibility (EN 301 549), and more.

How to build an accessible tab

Published

#tutorial #aria

When building web components, it's always a good idea to use the native HTML elements available. However, there are times when you may need to create custom components. In such cases, it's crucial to ensure that those components behave like their native counterparts, especially when it comes to accessibility.

For example, when creating a custom button, you'd need to add the role="button" ARIA attribute to mimic the native <button> element. Beyond that, you'd also want to ensure the component responds to keyboard interactions, such as the Enter or Space keys, to activate the button. This also includes setting up things like tabindex, managing focus, and handling other accessibility considerations.

This tutorial, however, isn't about building custom buttons, it's about building a tab component. As of writing, there's no native HTML element specifically for tabs, making it essential to implement ARIA roles and behavior to make the component accessible.

In this guide, I'll walk you through how to create an accessible tab component from scratch. The full code is available on Codepen

Prerequisite

To follow along with this tutorial, you'll need knowledge of:

  • Basic HTML, CSS, and JavaScript knowledge
  • ARIA

ARIA Authoring Practices (APG)

When I need to build components that aren't available as native HTML elements, I typically check the ARIA Authoring Practices Guide (APG) to review the accessibility considerations for the component. In this case, we'll be looking at the Tabs Pattern.

The APG outlines the roles we'll be using for our tab component:

  • Tablist: A list of tab elements that reference corresponding tabpanel elements.
  • Tab: A label that provides a mechanism for selecting the tab content that will be displayed.
  • Tabpanel: A container for the content associated with a tab, where each tab is linked to its respective tabpanel.

The APG also specifies key ARIA attributes for each role:

  • The tablist SHOULD include an aria-labelledby attribute if the list has a visible label, or an aria-label if it doesn't.
  • Each tab MUST include aria-selected (true or false), tabindex, and aria-controls attributes:
    • aria-selected: to indicate the current state of the tab (whether it's active or not).
    • tabindex: The active tab should have tabindex="0" and inactive tabs should have tabindex="-1", to prevent them from being focusable until activated.
    • aria-controls: should point to the id of the respective tabpanel, this will link the tab to its associated tabpanel.
  • Each tabpanel MUST have tabindex="0" to ensure it's focusable when activated, and aria-labelledby. The aria-labelledby attribute should link to the id of the respective tab, to indicate which tab controls the panel.

Tab Behaviors

APG provides two examples of the tab behaviour:

  • Tabs With Automatic Activation: The tabs are automatically activated as they receive focus, and the respective panel is displayed. This behavior should ideally be the default. However, this behaviour is only recommended when switching tabs does not cause content loading delays or layout shifts. If delays are present, manual activation should be used to avoid a frustrating user experience.
  • Tabs With Manual Activation: The tabs are activated by pressing the Space or Enter key. This behavior works well in scenarios where the user might need to carefully choose or confirm the content they wish to view, such as in a form with dynamic content that could change based on the user's choice.

Accessibility Features

  • Tab Indexing: The active tab has a tabindex="0", while the other tabs have tabindex="-1". This ensures that keyboard and screen reader users can move between content efficiently without having to go through all of the tabs.
  • Tabpanel Focus: The tabpanel has a tabindex="0" to make it easy for screen reader users to move from a tab to the beginning of the tabpanel content.
  • Keyboard Navigation: Keyboard users can navigate between tabs using the left and right arrow keys. The tab navigation is looped, so if the user is at the beginning of the tab list and presses the left arrow key, focus will move to the last tab.
  • Visible Focus State: The active tab has a visible style that doesn't rely on color alone, so users with low vision, color blindness, or high contrast settings can easily distinguish the active tab from the others.
  • Responsive Design: Relative units are used to specify the component's width (if provided), ensuring that the tab content remains accessible and visible when the screen is magnified.
  • Clear Labeling: The aria-labelledby attribute is used to associate the tabpanel with the corresponding tab, making it clear for screen reader users which content corresponds to which tab.
  • Focus Management: When a tab is activated (either through keyboard or mouse interaction), the focus should move to the tabpanel or the first interactive element inside the tabpanel, this ensures a smooth navigation for screen reader and keyboard users.
  • Consistent Order: Ensure that the tab focus follows a logical, visual order. For example, a keyboard user should not move from tab 1 to tab 6 and then to tab 3 if that isn't the visible order of the tabs. The tablist should follow the visual sequence, and each tabpanel should correspond to the active tab in that same order, to ensure a predictable navigation.
  • Auto-activation of Tabs: APG recommends that tabs should automatically activate when they receive focus, except when switching tabs could cause content delay, layout shifts, or page reloads. This allows for a simpler navigation experience for keyboard users.

Keyboard Interaction

  • Tab:
    • Moves focus to the active tab element
    • If a tab is focused, the focus moves to the next focusable element which should be the tabpanel, or the first focusable element in the tabpanel
  • In the tab list
    • Left Arrow: Moves focus to the previous tab. If the first tab is focused, focus moves to the last tab in the list (looping behavior)
    • Right Arrow: Moves focus to the next tab. If the last tab is focused, focus moves to the first tab
    • Home: Moves focus to the first tab in the tab list.
    • End: Moves focus to the last tab in the tab list.
    • Space or Enter: Activates the focused tab if it wasn't activated automatically on focus
    • Delete(Optionally): If deletion is allowed, deletes the current tab and its associated tabpanel. After deletion, the next or previous tab is activated, depending on availability

Implementation

In this implementation, I used Tailwind CSS for styling the elements, and I've ensured that the layout is flexible and responsive. The primary focus here is on the accessibility and functionality of the tab interaction, which I've implemented using TypeScript.

The activateTab Function

In the code, both the tab components use the activateTab function. The function takes three params (tabs, panels, index) however, you don't need that for your implementation. Ideally, you'll only need the index parameter.

The function does the following:

  • Sets the aria-selected attribute: This tells screen readers and other assistive technologies which tab is selected. The active tab gets aria-selected="true", and inactive tabs get aria-selected="false".
  • Manages tabindex for Focus: The active tab has a tabindex="0", which means it is focusable and navigable via keyboard. The inactive tabs get a tabindex="-1", which makes them non-focusable until they're activated.
  • Visibility of Content: Each tab has a .active class that is toggled to add an active style to the active tab. Each tabpanel has a corresponding .active-panel class that is toggled to show or hide the content based on the active tab. Only the tabpanel that corresponds to the selected tab is visible.
  • Focus Handling: Finally, after making the tab active, tabs[index].focus() ensures that focus is moved to the selected tab, so the user can immediately interact with it, whether via keyboard or screen reader.

Feel free to explore the working examples on CodePen and experiment with both versions of the tab component.


If you found this tutorial helpful, connect with me for more accessibility tips and updates!