A11Y: Make tabs accessible
This will fix statamic/a11y#1
Currently, publish tabs output this markup which makes it impossible to navigate if you're not using a mouse.
<div>
<div class="publish-tabs tabs">
<a class="active">First tab</a>
<a class="">Second tab</a>
<a class="">Third tab</a>
</div>
<div class="flex justify-between">
<div class="publish-section rounded-tl-none" style="">...</div>
<div class="publish-section rounded-tl-none" style="">...</div>
<div class="publish-section rounded-tl-none" style="">...</div>
<div class="publish-section rounded-tl-none" style="">...</div>
</div>
</div>
What we need to do
The element that serves as the container for the set of tabs has role tablist.
Each element that serves as a tab has role tab and is contained within the element with role
tablist.
Each element that contains the content panel for a
tabhas role tabpanel.
If the tab list has a visible label, the element with role
tablisthas aria-labelledby set to a value that refers to the labelling element. Otherwise, thetablistelement has a label provided by aria-label. [1]
<div>
<div class="publish-tabs tabs" role="tablist" aria-label="Edit entry">
<button class="active" role="tab">Main</button>
<button role="tab">Settings</button>
<button role="tab">Example third tab</button>
</div>
<div class="flex justify-between">
<div role="tabpanel" >...</div>
<div role="tabpanel">...</div>
<div role="tabpanel">...</div>
</div>
</div>
[1] We need to best describe what the tabs are & what they do in a very short label.
Continued:
Each element with role
tabhas the property aria-controls referring to its associatedtabpanelelement.
This means we need to add id attributes to the tabpanel elements, so aria-controls knows where to look. [2]
The active
tabelement has the state aria-selected set totrueand all othertabelements have it set tofalse.
Each element with role
tabpanelhas the property aria-labelledby referring to its associatedtabelement.
Again we need to add id attributes, but this time to the tabs themselves. [2]
<div>
<div class="publish-tabs tabs" role="tablist" aria-label="Edit entry">
<button class="active" role="tab" aria-controls="Main-Tab" aria-selected="true" id="main">Main</button>
<button role="tab" aria-controls="Settings-Tab" aria-selected="false" id="Settings">Settings</button>
<button role="tab" aria-controls="ExampleThirdTab-Tab" aria-selected="false" id="ExampleThirdTab">Example third tab</button>
</div>
<div class="flex justify-between">
<div role="tabpanel" id="Main-Tab" aria-labelledby="Main">...</div>
<div role="tabpanel" id="Settings-Tab" aria-labelledby="Settings">...</div>
<div role="tabpanel" id="ExampleThirdTab-Tab" aria-labelledby="ExampleThirdTab">...</div>
</div>
</div>
[2] id attributes should be CamelCased to give screen readers better clues that they should be read as individual words.
Lastly: Implementing roving tabindex
Roving tab index is the concept of having only the outermost element focusable via tab. This helps users by not forcing them to tab into every focusable element to get to the main content.
How this works in tabs
- When a user
tabsinto the tabs component the element withrole="tab"is focused. - The user is then able to navigate the tabs by using the following keys:
left arrow,right arrow,homeandend. - The next time a user presses
tab, the next element that is focused is the element withrole="tabpanel"that corresponds with the currently activetab.
How to accomplish this
-
All elements with
role="tab"should havetabindex="-1"set, except for the first active tab, where notabindexattribute is needed. -
All elements with
role="tabpanel"should be hidden. Except for the first active tab -
When a navigation key (see above) is activated to navigate the tabs: 3.1) The tab you're moving to should have the
tabindexattribute removed. 3.2) The tab you're moving from should have thetabindexattribute set to-1. 3.3) The tabpanel you're moving to should be unhidden. 3.4) The tabpanel you're moving from should be hidden. -
All elements with
role="tabpanel"should havetabindex="0"set.
Functionality
- Left Arrow: Moves focus to the previous tab.
- Right Arrow: Moves focus to the next tab.
- Space or Enter: Activates the tab if it was not activated automatically on focus.
- (optional) Home: Activated the first tab in the tab list.
- (optional) End: Activated the last tab in the tab list.
-
aria-selectedneeds to change totrueorfalsewhen the tab is activated or inactivated. - Roving tabindex (see above)
Taking it a step further
(From https://www.w3.org/TR/wai-aria-practices/#tabpanel)
It is recommended that tabs activate automatically when they receive focus as long as their associated tab panels are displayed without noticeable latency. This typically requires tab panel content to be preloaded. Otherwise, automatic activation slows focus movement, which significantly hampers users' ability to navigate efficiently across the tab list. For additional guidance, see § 6.4 Deciding When to Make Selection Automatically Follow Focus.
Extended reading
Tabs
https://www.w3.org/TR/wai-aria-practices/#tabpanel
https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-1/tabs.html
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role
Roving tabindex
https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex
https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex
Hi Jon, just dropping in to say that’s some awesome research and you’ve summarized it nicely👍. If I find some spare time I’ll try to implement this (unless someone else already did)!