Tablist

A tablist is a group of tabs that are used to navigate between layered sections of content, known as tab panels. Each tab controls the visibility of a tab panel. Only one tab panel can be visible at a time. The tablist and its associated tab panels are referred to as a tabbed interface.

The tablist orientation defaults to horizontal, but could also be vertical. A horizontal tablist is usually placed directly above the currenty displayed tab panel, while a vertical tablist will be placed to its left.

Role

A group of tabs must be enclosed within a container element. The container element must have a role of tablist.

1<!-- a tablist using a div container -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<div role="tablist" aria-labelledby="tablist-heading">
4 <!-- tabs go here -->
5</div>
6
7<!-- a tablist using a list container -->
8<h2 id="tablist-heading">Orchestra instruments</h2>
9<ul role="tablist" aria-labelledby="tablist-heading">
10 <!-- tabs go here -->
11</ul>

Accessibility label

The container element should have an accessibility label, though it is not required. If the tablist has a visible label then an accessibility label should be provided using the aria-labelledby attribute that points to an element that contains the visible label text. If there is not visible label then the aria-label attribute should be used.

1<!-- a tablist with a visible label -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<ul role="tablist" aria-labelledby="tablist-heading">
4 <!-- tabs go here -->
5</ul>
6
7<!-- a tablist without a visible label -->
8<ul role="tablist" aria-label="Orchestra instruments">
9 <!-- tabs go here -->
10</ul>

Tablist has tabs

The children of the container element must have a role of tab. The tab role must be applied to the element that represents the tab.

1<!-- a tablist using a div container -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<div role="tablist" aria-labelledby="tablist-heading">
4 <button role="tab" aria-selected="true" tabindex="0">Violin</button>
5 <button role="tab" aria-selected="false" tabindex="-1">Viola</button>
6 <button role="tab" aria-selected="false" tabindex="-1">Cello</button>
7 <button role="tab" aria-selected="false" tabindex="-1">Bass</button>
8</div>
9
10<!-- a tablist using a list container -->
11<h2 id="tablist-heading">Orchestra instruments</h2>
12<ul role="tablist" aria-labelledby="tablist-heading">
13 <li role="tab" aria-selected="true" tabindex="0">Violin</li>
14 <li role="tab" aria-selected="false" tabindex="-1">Viola</li>
15 <li role="tab" aria-selected="false" tabindex="-1">Cello</li>
16 <li role="tab" aria-selected="false" tabindex="-1">Bass</li>
17</ul>

Forbidden interactive children

Aside from the tabs, a tablist must not contain any interactive children such as links or buttons. Such children would be inaccessible to screen reader users since they will be excluded from the accessibility tree.

For clarity, this rule does not apply to tab panel elements since they are not children of the tablist container.

Tabs accessibility labels

Each tab in the tablist must have a unique accessibility label. Generally element that represents the tab (e.g. button, div, li) will support name from content, then its accessibility label could be given from a direct text node child, an image with an alternative text, or even multiple children elements with an accessibility label. This ensures that the accessibility label is consistent with any visible label. Using aria-label or aria-labelledby to specify the accessibility label is acceptable but may lead to inconsistencies with any visible label so is not recommended.

1<!-- tabs labelled by visible text nodes -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<div role="tablist" aria-labelledby="tablist-heading">
4 <button role="tab" aria-selected="true" tabindex="0">Violin</button>
5 <button role="tab" aria-selected="false" tabindex="-1">Viola</button>
6 <button role="tab" aria-selected="false" tabindex="-1">Cello</button>
7 <button role="tab" aria-selected="false" tabindex="-1">Bass</button>
8</div>
9
10<!-- tabs labelled by an image with alternative text -->
11<h2 id="tablist-heading">Orchestra instruments</h2>
12<div role="tablist" aria-labelledby="tablist-heading">
13 <button role="tab" aria-selected="true" tabindex="0">
14 <img src="violin.png" alt="Violin" />
15 </button>
16 <button role="tab" aria-selected="false" tabindex="-1">
17 <img src="viola.png" alt="Viola" />
18 </button>
19 <button role="tab" aria-selected="false" tabindex="-1">
20 <img src="cello.png" alt="Cello" />
21 </button>
22 <button role="tab" aria-selected="false" tabindex="-1">
23 <img src="bass.png" alt="Bass" />
24 </button>
25</div>

Set size

Optionally, the aria-setsize attribute may be used to indicate the number of tabs in the tablist. If the aria-setsize attribute is used, then it should be set to the number of tabs in the tablist. The aria-setsize attribute must be set on the tabs themselves, not the tablist container and it should be consistent across all tabs in the group.

Set position

Optionally, the aria-posinset attribute may be used to indicate the position of a tab in the tablist. If the aria-posinset attribute is used, then it should be set to the position of the tab in the tablist.

ARIA owns

If the tablist should contain tabs that cannot be implemented as descendants of the tablist, the aria-owns attribute can be used to indicate that the tablist owns the tabs. However, since the aria-owns attribute is not supported by some assistive technologies, it is best practice to avoid it. If it must be used, the aria-owns attribute should be set on the tablist and should point to the IDs of the owned tabs. Any elements referenced by the aria-owns attribute should have a role of tab.

1<!-- tabs are not descendants of the tablist container -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<div role="tablist" aria-labelledby="tablist-heading" aria-owns="violin viola cello bass" />
4<div id="violin" role="tab" aria-selected="true" tabindex="0">Violin</div>
5<div id="viola" role="tab" aria-selected="false" tabindex="-1">Viola</div>
6<div id="cello" role="tab" aria-selected="false" tabindex="-1">Cello</div>
7<div id="bass" role="tab" aria-selected="false" tabindex="-1">Bass</div>

Selected tabs

The aria-selected attribute must be set on the tab that is selected. The aria-selected attribute must be set to true on the selected tab only.

The selected tab must be in the focus sequence, see Focus management.

Unreachable group

When semantic HTML controls such as button elements are used to implement tabs, it is possible to disable a tab by setting the disabled attribute. However, it is not acceptable for the selected tab to be disabled or not reacheable by keyboard navigation. This would make the entire tablist unreachable by keyboard since the selected tab must receive focus when the tablist receives focus.

1✓ <button role="tab" aria-selected="true" tabindex="0">Violin</button>
2<!-- not reachable by keyboard due to being disabled -->
3✗ <button role="tab" aria-selected="true" tabindex="0" disabled>Violin</button>
4<!-- not reachable by keyboard due to being excluded from the tab index -->
5✗ <button role="tab" aria-selected="true" tabindex="-1">Violin</button>

Controlled tab panels

Each tab in the tablist should control the visibility of a tab panel. The relationship between tab and tab panel should be indicated using the using the aria-controls attribute on the tab, which should point to the ID of the controlled tab panel.

Tab panel roles

Tab panels must have a role of tabpanel.

Tab panels accessibility labels

Tab panels must have an accessibility label. Since the name of the tab panel should be the same as the label of the tab that controls it, the accessibility label of the tab panel should be provided by using the aria-labelledby attribute that points to the ID of the tab that controls it.

1<!-- a tablist using a div container -->
2<h2 id="tablist-heading">Orchestra instruments</h2>
3<div role="tablist" aria-labelledby="tablist-heading">
4 <button id="violin" role="tab" aria-selected="true" tabindex="0">Violin</button>
5 <button id="viola" role="tab" aria-selected="false" tabindex="-1">Viola</button>
6 <button id="cello" role="tab" aria-selected="false" tabindex="-1">Cello</button>
7 <button id="bass" role="tab" aria-selected="false" tabindex="-1">Bass</button>
8</div>
9<div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0">...</div>
10<div id="viola-panel" role="tabpanel" aria-labelledby="viola" hidden>...</div>
11<div id="cello-panel" role="tabpanel" aria-labelledby="cello" hidden>...</div>
12<div id="bass-panel" role="tabpanel" aria-labelledby="bass" hidden>...</div>

perceivable tab panels

The active tab panel must be visible and readable by screen readers i.e. aria-hidden="false" (default).

1✓ <div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0">...</div>
2<!-- active tab panel not visible to screen reader -->
3✗ <div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0" aria-hidden="true">...</div>
4<!-- active tab panel visible only to screen reader -->
5✗ <div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0" style="opacity: 0;">...</div>

Any inactive tab panel must be not visible and hidden from screen readers. Good techniques for making the content invisible i.e. adding style attributes e.g display: none or visibility: hidden, or using the hidden attribute, will also hide the content from screen readers and ensure the tab panel is inert and not focusable.

If other methods are used to make the tab panel invisible such as setting the opacity to 0, then the tab panel must be explicity hidden from the sreen reader using the aria-hidden="true" attribute. The tab panel and its descendents must also be excluded from the focus sequence either by setting the inert attribute on the tab panel or by setting the tabindex="-1" on any potentially focsuable descendents.

1<!-- using the hidden attribute -->
2✓ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" hidden>...</div>
3<!-- using style attributes (can also be achieved by adding/removing CSS classes) -->
4✓ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="display: none;">...</div>
5✓ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="visibility: hidden;">...</div>
6<!-- using aria-hidden="true" and inert -->
7✓ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="opacity: 0;"
8 aria-hidden="true" inert>...</div>
9<!-- using aria-hidden="true" and tabindex="-1" -->
10✓ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="opacity: 0;"
11 aria-hidden="true">
12 ...
13 <button tabindex="-1">Click me</button>
14 </div>
15<!-- problematic examples -->
16<!-- not using aria-hidden -->
17✗ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="opacity: 0;">...</div>
18<!-- not excluding elements from the focus sequence -->
19✗ <div id="viola-panel" role="tabpanel" aria-labelledby="viola" style="opacity: 0;"
20 aria-hidden="true">
21 ...
22 <button>Click me</button>
23 </div>

Focus management

When focusing into a tablist, focus should be placed on the selected tab. If no tab is selected, then the focus should be placed on the first tab. Once focus is in the group, the next Tab key press should move focus to the active tab panel, which is controlled by the selected tab. This is usually achieved by setting the tabindex="0" on the active tab panel (recommended), or by having a focusable element within the tab panel.

If focus is in the tablist and the Shift key is held while pressing Tab, focus should move to the previous focusable element before the tablist.

There are two options for implementing focus management on the tabs:

  • Make the selected tab focusable by adding the tabindex="0" attribute. Make the other tabs unfocusable by adding the tabindex="-1" attribute.
1<h2 id="tablist-heading">Orchestra instruments</h2>
2<div role="tablist" aria-labelledby="tablist-heading">
3 <button role="tab" aria-selected="true" tabindex="0">Violin</button>
4 <button role="tab" aria-selected="false" tabindex="-1">Viola</button>
5 <button role="tab" aria-selected="false" tabindex="-1">Cello</button>
6 <button role="tab" aria-selected="false" tabindex="-1">Bass</button>
7</div>
8<div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0">...</div>
9<div id="viola-panel" role="tabpanel" aria-labelledby="viola" hidden>...</div>
10<div id="cello-panel" role="tabpanel" aria-labelledby="cello" hidden>...</div>
11<div id="bass-panel" role="tabpanel" aria-labelledby="bass" hidden>...</div>
  • Make the tablist container element focusable by adding the tabindex="0" attribute. Use the aria-activedescedant attribute on the container with the ID of the selected tab. This will cause the browser to move focus to the selected tab when the tablist container is focused.
1<h2 id="tablist-heading">Orchestra instruments</h2>
2<ul role="tablist" aria-labelledby="tablist-heading" tabindex="0" aria-activedescendant="violin">
3 <li id="violin" role="tab" aria-selected="true">Violin</li>
4 <li id="viola" role="tab" aria-selected="false">Viola</li>
5 <li id="cello" role="tab" aria-selected="false">Cello</li>
6 <li id="bass" role="tab" aria-selected="false">Bass</li>
7</ul>
8<div id="violin-panel" role="tabpanel" aria-labelledby="violin" tabindex="0">...</div>
9<div id="viola-panel" role="tabpanel" aria-labelledby="viola" hidden>...</div>
10<div id="cello-panel" role="tabpanel" aria-labelledby="cello" hidden>...</div>
11<div id="bass-panel" role="tabpanel" aria-labelledby="bass" hidden>...</div>

Keyboard navigation

Once focus is in the tablist, the Arrow keys may be used to move focus and optionally selection between tabs. If selection does not move automatically with focus, then the Space or Enter key should change the selection to the focused tab. If selection moves automatically with focus then it should be consistent for all keyboard navigation operations.

Tablists may be horizontally or vertically oriented. The orientation of the tablist should be indicated using the aria-orientation attribute, see Keyboard navigation orientation.

In a horizontal tablist (default):

  • The ArrowRight key should move focus and optionally change the selection to the next tab. If focus is on the last tab, it should loop around to the first tab.
  • The ArrowLeft key should move focus and optionally change the selection to the previous tab. If focus is on the first tab, it should loop around to the last tab.
  • It is best not to modify the behavior of the ArrowUp and ArrowDown so that their original functionality is maintained.

In a vertical tablist:

  • The ArrowDown key should move focus and optionally change the selection to the next tab. If focus is on the last tab, it should loop around to the first tab.
  • The ArrowUp key should move focus and optionally change the selection to the previous tab. If focus is on the first tab, it should loop around to the last tab.
  • It is best not to modify the behavior of the ArrowRight and ArrowLeft so that their original functionality is maintained.

In both horizontal and vertical tablists extended keyboard support may be provided:

  • The Home key may move focus and optionally change the selection to the first tab.
  • The End key may move focus and optionally change the selection to the last tab.
1const tablist = document.querySelector('[role="tablist"]');
2const tabs = tablist.querySelectorAll('[role="tab"]');
3const panels = document.querySelectorAll('[role="tabpanel"]');
4const tablistOrientation = tablist.getAttribute("aria-orientation");
5
6// using tabindex="0" on the active tab
7function selectTab(selectedTab) {
8 // deselect all tabs
9 tabs.forEach(function (tab) {
10 tab.setAttribute("aria-selected", "false");
11 tab.setAttribute("tabindex", "-1");
12 });
13
14 // set selected tab as selected
15 selectedTab.setAttribute("aria-selected", "true");
16 selectedTab.setAttribute("tabindex", "0");
17 selectedTab.focus();
18
19 // hide all panels
20 tabPanels.forEach(function (panel) {
21 panel.setAttribute("hidden", "");
22 panel.setAttribute("tabindex", "-1");
23 });
24
25 // show selected panel
26 var selectedPanel = document.getElementById(
27 selectedTab.getAttribute("aria-controls")
28 );
29 selectedPanel.removeAttribute("hidden");
30 selectedPanel.setAttribute("tabindex", "0");
31}
32
33// using aria-activedescendant on the tablist
34function selectTab(selectedTab) {
35 // deselect all tabs
36 tabs.forEach(function (tab) {
37 tab.setAttribute("aria-selected", "false");
38 });
39
40 // set selected tab as selected
41 selectedTab.setAttribute("aria-selected", "true");
42 tablist.setAttribute("aria-activedescendant", selectedTab.id);
43
44 // hide all panels
45 tabPanels.forEach(function (panel) {
46 panel.setAttribute("hidden", "");
47 panel.setAttribute("tabindex", "-1");
48 });
49
50 // show selected panel
51 var selectedPanel = document.getElementById(
52 selectedTab.getAttribute("aria-controls")
53 );
54 selectedPanel.removeAttribute("hidden");
55 selectedPanel.setAttribute("tabindex", "0");
56}
57
58// add click event listeners to tabs
59tabs.forEach(function (tab) {
60 tab.addEventListener("click", function (event) {
61 selectTab(event.currentTarget);
62 });
63});
64
65// add keydown event listeners to tabs for keyboard navigation
66tabs.forEach(function (tab) {
67 tab.addEventListener("keydown", function (event) {
68 // get the index of the current tab in the tablist
69 var index = Array.prototype.indexOf.call(tabs, event.currentTarget);
70 var length = tabs.length;
71
72 // check for arrow key keydowns
73 if (
74 (tablistOrientation === "horizontal" &&
75 event.key === "ArrowRight") ||
76 (tablistOrientation === "vertical" && event.key === "ArrowDown")
77 ) {
78 event.preventDefault();
79 if (index === length - 1) {
80 // move focus to the first tab
81 selectTab(tabs[0]);
82 } else {
83 // move focus to the next tab
84 selectTab(tabs[index + 1]);
85 }
86 } else if (
87 (tablistOrientation === "horizontal" &&
88 event.key === "ArrowLeft") ||
89 (tablistOrientation === "vertical" && event.key === "ArrowUp")
90 ) {
91 event.preventDefault();
92 if (index === 0) {
93 // move focus to the last tab
94 selectTab(tabs[length - 1]);
95 } else {
96 // move focus to the previous tab
97 selectTab(tabs[index - 1]);
98 }
99 }
100 });
101});

Keyboard navigation orientation

When implementing a vertical tablist, the orientation of the tablist must be indicated using the aria-orientation="vertical" attribute. For horizontal tablists, the aria-orientation="horizontal" attribute may be set but it is not required since horizontal is the default. It is crucial that the keyboard navigation is consistent with the declared orientation of the tablist.

Summary of Tests

NameConformance LevelWCAG Success Criteria
RoleA4.1.2 Name, Role, Value
Accessibility labelA4.1.2 Name, Role, Value
Tablist has tabsA4.1.2 Name, Role, Value, 1.3.1 Info and Relationships
Forbidden interactive childrenA4.1.2 Name, Role, Value
Tabs accessibility labelsA4.1.2 Name, Role, Value
Set sizeA1.3.1 Info and Relationships
Set positionA1.3.1 Info and Relationships
ARIA ownsA1.3.1 Info and Relationships
Selected tabsA4.1.2 Name, Role, Value
Unreachable tabsA2.1.1 Keyboard
Controlled tab panelsA1.3.1 Info and Relationships
Tab panel rolesA4.1.2 Name, Role, Value, 1.3.1 Info and Relationships
Tab panels accessibility labelsA4.1.2 Name, Role, Value, 1.3.1 Info and Relationships
Perceivable tab panelsA1.3.2 Meaningful Sequence
Focus managementA2.1.1 Keyboard, 2.4.3 Focus Order
Keyboard navigationA2.1.1 Keyboard
Keyboard navigation orientationA2.1.1 Keyboard