Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEβ€’Composite RolesContent switcherRoving focusARIA relationships

tablist

A collection of tabs that each control the visibility of a corresponding tabpanel. Tablists organize related content into digestible sections.

Live Example

Workspace Tablist

Tabs with roving tabindex, arrow-key navigation, and ARIA-linked tabpanels.

High-level overview of your workspace health.

Structure

tablist contains tabs. Each tab controls a tabpanel via aria-controls.

Behavior

Only one tabpanel should be visible at a time (unless multi-select tabs).

Screen Reader

Announces β€œtab 2 of N”. Users expect arrow keys to change tabs.

When to Use

1

Dashboard widgets

Switch between charts inside a compact card.

2

Settings views

Organize profile, billing, and security settings.

3

Documentation

Show code samples in different languages or frameworks.

Required Structure

Tab elements

role="tab" items, each with aria-controls referencing its tabpanel. Manage aria-selected + tabindex.

Panels

role="tabpanel" with id matching aria-controls. Hidden panels stay in DOM but are aria-hidden when inactive.

Orientation

Declare aria-orientation when tabs are vertical to help assistive tech.

Keyboard Interaction Model

ActionKeysResult
Move between tabsArrowLeft / ArrowRight (horizontal) or ArrowUp / ArrowDown (vertical)Focuses and optionally activates the adjacent tab.
Jump to start/endHome / EndMoves focus to the first or last tab in the list.
Activate tabSpace / EnterDisplays the associated tabpanel when activation follows focus.
Ctrl + Page Up/Page DownCtrl + PageUp / Ctrl + PageDownSwitches tabs in many desktop apps; mimic when building complex tooling.

Required States & Properties

aria-controlsRequired

Connects a tab to its tabpanel by id.

aria-selectedRequired

Set true on the active tab and false on the rest.

aria-expandedOptional

Optional when tabs double as accordions; indicates whether the panel is open.

aria-orientationOptional

Set to vertical when tabs are stacked.

aria-labelledbyOptional

Each tabpanel should reference the id of its owning tab.

Implementation Checklist

  • βœ“

    Use roving tabindex so only the active tab is focusable (tabindex="0"). Others should be -1.

  • βœ“

    If activating on focus causes performance concerns, allow manual activation via Enter/Space.

  • βœ“

    Provide clear focus and selection indicators for both keyboard and pointer interactions.

  • βœ“

    Maintain equal heights to avoid layout shifts when switching between tab contents.

  • βœ“

    If panels contain forms, manage focus so submitting inside a panel does not jump back to the tabs.

Code Examples

Horizontal Tablist

Tabs manage aria-selected and control matching panels.

<div role="tablist" aria-label="Billing tabs">
  <button role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0">
    Overview
  </button>
  <button role="tab" id="tab-invoices" aria-controls="panel-invoices" aria-selected="false" tabindex="-1">
    Invoices
  </button>
  <button role="tab" id="tab-settings" aria-controls="panel-settings" aria-selected="false" tabindex="-1">
    Settings
  </button>
</div>

<section id="panel-overview" role="tabpanel" aria-labelledby="tab-overview">
  <!-- content -->
</section>
<section id="panel-invoices" role="tabpanel" aria-labelledby="tab-invoices" hidden>
  <!-- content -->
</section>
<section id="panel-settings" role="tabpanel" aria-labelledby="tab-settings" hidden>
  <!-- content -->
</section>

React Tablist Controller

State-driven tablist with keyboard management.

import { useState } from 'react'

const tabs = [
  { id: 'overview', label: 'Overview' },
  { id: 'activity', label: 'Activity' },
  { id: 'logs', label: 'Logs' },
]

export function TabExample() {
  const [active, setActive] = useState('overview')

  return (
    <>
      <div role="tablist" aria-label="Project sections">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            role="tab"
            id={`tab-${tab.id}`}
            aria-controls={`panel-${tab.id}`}
            aria-selected={active === tab.id}
            tabIndex={active === tab.id ? 0 : -1}
            onClick={() => setActive(tab.id)}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {tabs.map((tab) => (
        <section
          key={tab.id}
          id={`panel-${tab.id}`}
          role="tabpanel"
          aria-labelledby={`tab-${tab.id}`}
          hidden={active !== tab.id}
        >
          Panel: {tab.label}
        </section>
      ))}
    </>
  )
}

Best Practices

πŸ“

Equal panel heights

Prevent layout jumps by reserving space for the tallest panel.

🧭

Visible selection

Use underline/indicator to show the active tab for sighted users.

πŸ“

Announce changes

Let users know when new content loads via aria-live if asynchronous.

Common Mistakes

Hiding panels with display:none

Completely removing the panel from the accessibility tree prevents screen readers from finding content.

Use hidden attribute or aria-hidden + CSS visibility while keeping the DOM node available.

Every tab tabbable

Users must tab through all tabs before reaching content.

Only the active tab should be tabbable; use arrow keys for internal navigation.

Related Roles & Patterns

tab

Interactive element inside the tablist.

Learn more

tabpanel

Container for content associated with a tab.

Learn more

aria-orientation

Attribute used to announce vertical tablists.

Learn more