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
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.
Photo by Ramiz Dedaković on Unsplash

Keyboard Shortcuts

Keyboard shortcuts for tabs
KeyBehaviour
Tab
  • When focus moves to the tabs-widget, the active tab-element gets focus.
  • When focus is in the tab-element, focus moves to next focusable item (so, not to next tab). This can mean either item in the active tab panel, or first thing outside the widget.
Left ArrowWhen 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 ArrowWhen 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 SpacebarIf tabs are not activated automatically when focused, activates the focused tab.
Shift + F10If 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:

Optional keyboard shortcuts for tabs
KeyBehaviour
HomeMoves focus to the first tab.
EndMoves focus to the last tab.
DeleteIf 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%;
}