Tabs
Table of Contents
Tabs, or Tab lists, are a collection of layered content (tab panels). These tab panels are shown one at a time, depending on the tab list's currently selected tab. For the example on this page, I've used the Tabs-design pattern Opens an external site from WAI-ARIA Authoring Practices. I've written a blog post Opens an external site explaining the pattern a bit more.
Cats
The content here can be anything:
- A link
- A
- or cat, if you want
Moar Cats
It can be also more cats:Photo by Justin Sinclair on UnsplashOr Even Moar Cats!
Photo by Jari Hytönen on UnsplashKeyboard Shortcuts
Key | Behaviour |
---|---|
Tab |
|
Left Arrow | When focus is on the active tab -element, moves focus to the next item on the list. If on the last item, moves focus to the first item. If tabs are automatically activated, activates focused tab (see note). |
Right Arrow | When focus is on the active tab -element, moves focus to the previous item on the list. If on the first item, moves focus to the last item. If tabs are automatically activated, activates focused tab (see note). |
Enter or Spacebar | If tabs are not activated automatically when focused, activates the focused tab. |
Shift + F10 | If there is a pop-up menu associated with the tab, this shortcut opens it. |
In this example, I haven't implemented the Shift + F10-shortcut, as there is no pop-up menu associated with the tabs. There are also optional keyboard shortcuts, which will not be implemented in this example:
Key | Behaviour |
---|---|
Home | Moves focus to the first tab. |
End | Moves focus to the last tab. |
Delete | If it is possible to delete the tab element, Delete deletes the current tab element and its tab panel. It can activate the tab focus goes to. |
Note about automatic activation
Tabs can be activated either automatically, or with a Enter or Spacebar key. According to Tabs-design pattern Opens an external site, it is recommended to use the automatic activation of the tabs, meaning that the tab panel is shown when the tab controlling it is focused. It, however, means that the content of the panel needs to be available and preloaded. Otherwise, the loading of the content takes so much time that it affects usability.
If you're interested in reading more about if you should make the focus automatically activate the tab panel, there's a section about it in WAI-ARIA Authoring Practices. Opens an external site
ARIA-states, properties, and roles
Roles
Tabs pattern consists of three elements with aria-roles. The container for the tabs needs to have the role tablist
. It wraps tabs, which each have the role tab
. All tab panels have the role of tabpanel
.
States and Properties
Suppose the tabs have a visible label communicating the tabs' purpose. In that case, the tabs' wrapper (an element with the tablist
-role) should have an aria-labelledby
attribute referencing to that label. If there is no visible label, then that element should have an `aria-label '-attribute communicating the tabs' purpose.
Every tab should have an aria-controls
-attribute referring to the tab panel element it controls. Note, however, that aria-controls
is supported only with the JAWS screen reader. If the tab element has a popup-menu, it should have the aria-haspopup
with either value menu
or true
.
One of the tabs can be active at the time. That tab should have the aria-selected
attribute set to true
, and others should have it set to false
. If the tab list is vertical, it needs the aria-orientation
set to vertical
- the default value is horizontal.
Every tab panel should have the aria-labelledby
referencing to the tab
-element that controls it.
Source Code
In this example, the code is divided into three different components: Tab, TabPanel, and Tabs (the wrapper). I've also added the source code of CSS for the looks.
When navigating between the tabs with the keyboard, the focus needs to be managed. React provides a great way to accomplish this: Refs, and more specifically, useRef
-hook.
Tab
// Tab.tsx
import React, { FunctionComponent, LegacyRef } from "react";
interface TabProps {
id: string;
title: string;
selectedTab: number;
index: number;
tabPanelId: string;
handleChange: (event) => void;
tabRef: LegacyRef<HTMLButtonElement>;
}
const Tab: FunctionComponent<TabProps> = ({
id,
title,
selectedTab,
index,
tabPanelId,
handleChange,
tabRef,
}) => {
const handleClick = () => handleChange(index);
return (
<li role="presentation">
<button
role="tab"
id={id}
aria-selected={selectedTab === index}
aria-controls={tabPanelId}
tabIndex={selectedTab === index ? 0 : -1}
onClick={handleClick}
ref={tabRef}
>
{title}
</button>
</li>
);
};
export default Tab;
Tab Panel
// TabPanel.tsx
import React, { FunctionComponent } from "react";
interface TabPanelProps {
id: string;
tabId: string;
tabIndex: number;
selectedTab: number;
}
const TabPanel: FunctionComponent<TabPanelProps> = ({
children,
id,
tabId,
tabIndex,
selectedTab,
}) => (
<section
role="tabpanel"
id={id}
aria-labelledby={tabId}
hidden={selectedTab !== tabIndex}
tabIndex={0}
>
{children}
</section>
);
export default TabPanel;
Tabs
// Tabs.tsx
import Image from "next/image";
import React, { useRef, useState, KeyboardEvent } from "react";
import TabPanel from "./TabPanel";
import Tab from "./Tab";
const Tabs = () => {
const [selectedTab, setSelectedTab] = useState(1);
const handleClick = (index: number) => {
setSelectedTab(index);
};
const tabValues = {
1: { title: "Cats", ref: useRef(null) },
2: { title: "Moar Cats", ref: useRef(null) },
3: { title: "Or Even Moar Cats!", ref: useRef(null) },
};
const handleKeyPress = (event: KeyboardEvent<HTMLUListElement>) => {
const tabCount = Object.keys(tabValues).length;
if (event.key === "ArrowLeft") {
const last = tabCount;
const next = selectedTab - 1;
handleNextTab(last, next, 1);
}
if (event.key === "ArrowRight") {
const first = 1;
const next = selectedTab + 1;
handleNextTab(first, next, tabCount);
}
};
const handleNextTab = (
firstTabInRound: number,
nextTab: number,
lastTabInRound: number
) => {
const tabToSelect =
selectedTab === lastTabInRound ? firstTabInRound : nextTab;
setSelectedTab(tabToSelect);
tabValues[tabToSelect].ref.current.focus();
};
return (
<section className="tabs-wrapper">
<div className="switcher">
<ul
role="tablist"
className="tablist switcher"
aria-label="Cat tabs"
onKeyDown={handleKeyPress}
>
<Tab
id="firstTab"
tabPanelId="firstTabPanel"
index={1}
handleChange={handleClick}
selectedTab={selectedTab}
tabRef={tabValues[1].ref}
title={tabValues[1].title}
/>
<Tab
id="secondTab"
tabPanelId="secondTabPanel"
index={2}
handleChange={handleClick}
selectedTab={selectedTab}
tabRef={tabValues[2].ref}
title={tabValues[2].title}
/>
<Tab
id="thirdTab"
tabPanelId="thirdTabPanel"
index={3}
handleChange={handleClick}
selectedTab={selectedTab}
tabRef={tabValues[3].ref}
title={tabValues[3].title}
/>
</ul>
</div>
<TabPanel
id="firstTabPanel"
tabId="firstTab"
tabIndex={1}
selectedTab={selectedTab}
>
<h2>{tabValues[1].title}</h2>
<p>The content here can be anything:</p>
<ul>
<li>
A <a href="/">link</a>
</li>
<li>
A <button>button</button>
</li>
<li>or cat, if you want</li>
</ul>
<Image
alt="Cat staring at the camera with eyes wide open and head tilted to the left. The cat is mostly grey, with black stripes, and is on human's lap."
src="/cats/cat-1.jpg"
layout="responsive"
height="450"
width="300"
/>
<span>
Photo by <a href="https://unsplash.com/@ramche">Ramiz Dedaković</a> on{" "}
<a href="https://unsplash.com/s/photos/cat">Unsplash</a>
</span>
</TabPanel>
<TabPanel
id="secondTabPanel"
tabId="secondTab"
tabIndex={2}
selectedTab={selectedTab}
>
<h2>{tabValues[2].title}</h2>
It can be also more cats:
<Image
alt="Two calico-colored cats. One of them is standing and looking at the camera, another sitting and looking away. They're in a corner of a room with white walls."
src="/cats/cat-2.jpg"
layout="responsive"
height="300"
width="450"
/>
<span>
Photo by{" "}
<a href="https://unsplash.com/@justinsinclair">Justin Sinclair</a> on{" "}
<a href="https://unsplash.com/s/photos/cats">Unsplash</a>
</span>
</TabPanel>
<TabPanel
id="thirdTabPanel"
tabId="thirdTab"
tabIndex={3}
selectedTab={selectedTab}
>
<h2>{tabValues[3].title}</h2>
<Image
alt="Four kittens in a basket on the grass. One of them is white with some orange on their back and one is almost completely orange. Other two are mostly white with some black on their backs. Three of them look up, and one looks on the ground."
src="/cats/cat-3.jpg"
layout="responsive"
height="301"
width="450"
/>
<span>
Photo by <a href="https://unsplash.com/@jarispics">Jari Hytönen</a> on{" "}
<a href="https://unsplash.com/s/photos/cats">Unsplash</a>
</span>
</TabPanel>
</section>
);
};
export default Tabs;
CSS
// CSS-file
.tablist {
display: flex;
flex-direction: row;
}
.tablist > li {
list-style: none;
}
.tablist > li > button {
margin: 0.5rem;
background-color: var(--color-bakcground);
border: none;
color: var(--color-text);
border-bottom: 0.25rem solid var(--color-text);
font-size: 1.5rem;
font-family: "Montserrat", sans-serif;
}
.tablist > li > [aria-selected="true"] {
border-bottom-color: var(--color-primary);
}
.tabs-wrapper {
border: 0.15rem solid var(--color-text);
padding: 1rem;
}
/* From Every Layout (https://every-layout.dev/) */
.switcher > * {
display: flex;
flex-wrap: wrap;
margin: calc((var(--tab-padding) / 2) * -1);
}
.switcher > * > * {
flex-grow: 1;
flex-basis: calc((var(--tab-width) - (100% - var(--tab-padding))) * 999);
margin: calc(var(--tab-padding) / 2);
}
.switcher > * > :nth-last-child(n + 5),
.switcher > * > :nth-last-child(n + 5) ~ * {
flex-basis: 100%;
}