Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLE•Widget 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 Arrow→Moves focus to the previous tab, wrapping to the last tab
Right Arrow→Moves focus to the next tab, wrapping to the first tab
Home→Moves focus to the first tab
End→Moves focus to the last tab
Tab→Moves 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