Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

tab

A grouping label providing a mechanism for selecting the tab content that is to be rendered to the user. Part of a tablist, controlling a tabpanel.

Keyboard Support
Arrow Keys
Required Attribute
aria-selected
Parent Role
tablist

Overview

The tab role indicates an interactive element that, when activated, displays its associated tabpanel. Tabs are always part of a tablist and work together to provide a tabbed interface pattern.

Only one tab in a tablist should be active at a time, indicated by aria-selected="true". All other tabs should have aria-selected="false".

Live Demo: Tab Interface

This is the overview tab content with general information.

Keyboard navigation: Use Left/Right Arrow to switch tabs, Home/End to go to first/last tab.

Code Examples

Basic Tab Pattern

<!-- Basic Tab Pattern -->
<div class="tabs">
  <!-- Tab List -->
  <div role="tablist" aria-label="Content sections">
    <button 
      role="tab" 
      aria-selected="true"
      aria-controls="panel-1"
      id="tab-1"
      tabindex="0"
    >
      Tab 1
    </button>
    <button 
      role="tab" 
      aria-selected="false"
      aria-controls="panel-2"
      id="tab-2"
      tabindex="-1"
    >
      Tab 2
    </button>
    <button 
      role="tab" 
      aria-selected="false"
      aria-controls="panel-3"
      id="tab-3"
      tabindex="-1"
    >
      Tab 3
    </button>
  </div>

  <!-- Tab Panels -->
  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0">
    Content for tab 1
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden>
    Content for tab 2
  </div>
  <div role="tabpanel" id="panel-3" aria-labelledby="tab-3" tabindex="0" hidden>
    Content for tab 3
  </div>
</div>

Full Keyboard Support

<!-- Tab with Full Keyboard Support -->
<script>
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  tabs.forEach((tab, index) => {
    tab.addEventListener('click', () => selectTab(index));
    
    tab.addEventListener('keydown', (e) => {
      let newIndex = index;
      
      if (e.key === 'ArrowRight') {
        e.preventDefault();
        newIndex = (index + 1) % tabs.length;
      } else if (e.key === 'ArrowLeft') {
        e.preventDefault();
        newIndex = (index - 1 + tabs.length) % tabs.length;
      } else if (e.key === 'Home') {
        e.preventDefault();
        newIndex = 0;
      } else if (e.key === 'End') {
        e.preventDefault();
        newIndex = tabs.length - 1;
      } else {
        return; // Don't handle other keys
      }
      
      selectTab(newIndex);
      tabs[newIndex].focus();
    });
  });

  function selectTab(index) {
    // Update tabs
    tabs.forEach((tab, i) => {
      const isSelected = i === index;
      tab.setAttribute('aria-selected', isSelected);
      tab.setAttribute('tabindex', isSelected ? '0' : '-1');
    });
    
    // Update panels
    panels.forEach((panel, i) => {
      panel.hidden = i !== index;
    });
  }
</script>

React Component

// React Tabs Component
import { useState } from 'react';

interface Tab {
  id: string;
  label: string;
  content: React.ReactNode;
}

function Tabs({ tabs }: { tabs: Tab[] }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
    let newIndex = index;
    
    if (e.key === 'ArrowRight') {
      e.preventDefault();
      newIndex = (index + 1) % tabs.length;
    } else if (e.key === 'ArrowLeft') {
      e.preventDefault();
      newIndex = (index - 1 + tabs.length) % tabs.length;
    } else if (e.key === 'Home') {
      e.preventDefault();
      newIndex = 0;
    } else if (e.key === 'End') {
      e.preventDefault();
      newIndex = tabs.length - 1;
    } else {
      return;
    }
    
    setActiveIndex(newIndex);
  };

  return (
    <div className="tabs">
      {/* Tab List */}
      <div role="tablist" aria-label="Content sections" className="flex border-b">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            role="tab"
            aria-selected={activeIndex === index}
            aria-controls={`panel-${tab.id}`}
            id={`tab-${tab.id}`}
            tabIndex={activeIndex === index ? 0 : -1}
            onClick={() => setActiveIndex(index)}
            onKeyDown={(e) => handleKeyDown(e, index)}
            className={`px-4 py-2 ${
              activeIndex === index 
                ? 'border-b-2 border-blue-500 text-blue-600' 
                : 'text-gray-600'
            }`}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {/* Tab Panels */}
      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeIndex !== index}
          tabIndex={0}
          className="p-4"
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

// Usage
function App() {
  const tabs = [
    { id: 'home', label: 'Home', content: <div>Home content</div> },
    { id: 'profile', label: 'Profile', content: <div>Profile content</div> },
    { id: 'settings', label: 'Settings', content: <div>Settings content</div> },
  ];

  return <Tabs tabs={tabs} />;
}

Keyboard Support

Left ArrowMoves focus to the previous tab, wrapping to the last tab
Right ArrowMoves focus to the next tab, wrapping to the first tab
HomeMoves focus to the first tab
EndMoves focus to the last tab
TabMoves focus from the tab into the content of the active tabpanel

Best Practices

Only one tab should have aria-selected="true" at a time

Inactive tabs should have tabindex="-1"

Tab labels should be concise and descriptive

Use aria-controls to link tabs to their tabpanels

Provide clear visual indication of the selected tab

×

Don't use tabs for navigation between pages - use links instead

×

Don't hide inactive tabpanels with CSS only - use the hidden attribute

×

Don't forget to implement arrow key navigation

Related Roles