useTabList

Provides the behavior and accessibility implementation for a tab list. Tabs organize content into multiple sections and allow users to navigate between them.

installyarn add react-aria
version3.19.0
usageimport {useTabList, useTab, useTabPanel} from 'react-aria'

API#


useTabList<T>( props: AriaTabListProps<T>, state: TabListState<T>, ref: RefObject<HTMLElement> ): TabListAria useTab<T>( props: AriaTabProps, state: TabListState<T>, ref: RefObject<FocusableElement> ): TabAria useTabPanel<T>( props: AriaTabPanelProps, state: TabListState<T>, ref: RefObject<Element> ): TabPanelAria

Features#


Tabs provide a list of tabs that a user can select from to switch between multiple tab panels. useTabList, useTab, and useTabPanel can be used to implement these in an accessible way.

  • Support for mouse, touch, and keyboard interactions on tabs
  • Support for LTR and RTL keyboard navigation
  • Support for disabled tabs
  • Follows the tabs ARIA pattern, semantically linking tabs and their associated tab panels
  • Focus management for tab panels without any focusable children

Anatomy#


Section 1Section 2TabTab (selected)Tab listTab panel

Tabs consist of a tab list with one or more visually separated tabs. Each tab has associated content, and only the selected tab's content is shown. Each tab can be clicked, tapped, or navigated to via arrow keys. Depending on the keyboardActivation prop, the tab can be selected by receiving keyboard focus, or it can be selected with the Enter key.

useTabList returns props to spread onto the tab list container:

NameTypeDescription
tabListPropsDOMAttributesProps for the tablist container.

useTab returns props to be spread onto each individual tab, along with states that can be used for styling:

NameTypeDescription
tabPropsDOMAttributesProps for the tab element.
isSelectedbooleanWhether the tab is currently selected.
isDisabledbooleanWhether the tab is disabled.
isPressedbooleanWhether the tab is currently in a pressed state.

useTabPanel returns props to spread onto the container for the tab content:

NameTypeDescription
tabPanelPropsDOMAttributesProps for the tab panel element.

State is managed by the useTabListState hook in @react-stately/tabs. The state object should be passed as an option to useTabList, useTab, and useTabPanel. The Item component is used to represent each tab, following the Collections API used by many other components.

Example#


This example displays a basic list of tabs. The currently selected tab receives a tabIndex of 0 while the rest are set to -1 ensuring that the whole tablist is a single tab stop. The selected tab has a different style so it's obvious which one is currently selected. useTab and useTabPanel handle associating the tabs and tab panels for assistive technology. The currently selected tab panel is rendered below the list of tabs. The key prop on the TabPanel element is important to ensure that DOM state (e.g. text field contents) is not shared between unrelated tabs.

import {useTab, useTabList, useTabPanel} from 'react-aria';
import {Item, useTabListState} from 'react-stately';

function Tabs(props) {
  let state = useTabListState(props);
  let ref = React.useRef();
  let { tabListProps } = useTabList(props, state, ref);
  return (
    <div class={`tabs ${props.orientation || ''}`}>
      <div {...tabListProps} ref={ref}>
        {[...state.collection].map((item) => (
          <Tab
            key={item.key}
            item={item}
            state={state}
            orientation={props.orientation}
          />
        ))}
      </div>
      <TabPanel key={state.selectedItem?.key} state={state} />
    </div>
  );
}

function Tab({ item, state, orientation }) {
  let { key, rendered } = item;
  let ref = React.useRef();
  let { tabProps, isSelected, isDisabled } = useTab({ key }, state, ref);
  return (
    <div {...tabProps} ref={ref}>
      {rendered}
    </div>
  );
}

function TabPanel({ state, ...props }) {
  let ref = React.useRef();
  let { tabPanelProps } = useTabPanel(props, state, ref);
  return (
    <div {...tabPanelProps} ref={ref}>
      {state.selectedItem?.props.children}
    </div>
  );
}

<Tabs aria-label="History of Ancient Rome">
  <Item key="FoR" title="Founding of Rome">
    Arma virumque cano, Troiae qui primus ab oris.
  </Item>
  <Item key="MaR" title="Monarchy and Republic">
    Senatus Populusque Romanus.
  </Item>
  <Item key="Emp" title="Empire">Alea jacta est.</Item>
</Tabs>
import {useTab, useTabList, useTabPanel} from 'react-aria';
import {Item, useTabListState} from 'react-stately';

function Tabs(props) {
  let state = useTabListState(props);
  let ref = React.useRef();
  let { tabListProps } = useTabList(props, state, ref);
  return (
    <div class={`tabs ${props.orientation || ''}`}>
      <div {...tabListProps} ref={ref}>
        {[...state.collection].map((item) => (
          <Tab
            key={item.key}
            item={item}
            state={state}
            orientation={props.orientation}
          />
        ))}
      </div>
      <TabPanel
        key={state.selectedItem?.key}
        state={state}
      />
    </div>
  );
}

function Tab({ item, state, orientation }) {
  let { key, rendered } = item;
  let ref = React.useRef();
  let { tabProps, isSelected, isDisabled } = useTab(
    { key },
    state,
    ref
  );
  return (
    <div {...tabProps} ref={ref}>
      {rendered}
    </div>
  );
}

function TabPanel({ state, ...props }) {
  let ref = React.useRef();
  let { tabPanelProps } = useTabPanel(props, state, ref);
  return (
    <div {...tabPanelProps} ref={ref}>
      {state.selectedItem?.props.children}
    </div>
  );
}

<Tabs aria-label="History of Ancient Rome">
  <Item key="FoR" title="Founding of Rome">
    Arma virumque cano, Troiae qui primus ab oris.
  </Item>
  <Item key="MaR" title="Monarchy and Republic">
    Senatus Populusque Romanus.
  </Item>
  <Item key="Emp" title="Empire">Alea jacta est.</Item>
</Tabs>
import {
  useTab,
  useTabList,
  useTabPanel
} from 'react-aria';
import {
  Item,
  useTabListState
} from 'react-stately';

function Tabs(props) {
  let state =
    useTabListState(
      props
    );
  let ref = React
    .useRef();
  let { tabListProps } =
    useTabList(
      props,
      state,
      ref
    );
  return (
    <div
      class={`tabs ${
        props
          .orientation ||
        ''
      }`}
    >
      <div
        {...tabListProps}
        ref={ref}
      >
        {[
          ...state
            .collection
        ].map((item) => (
          <Tab
            key={item
              .key}
            item={item}
            state={state}
            orientation={props
              .orientation}
          />
        ))}
      </div>
      <TabPanel
        key={state
          .selectedItem
          ?.key}
        state={state}
      />
    </div>
  );
}

function Tab(
  {
    item,
    state,
    orientation
  }
) {
  let { key, rendered } =
    item;
  let ref = React
    .useRef();
  let {
    tabProps,
    isSelected,
    isDisabled
  } = useTab(
    { key },
    state,
    ref
  );
  return (
    <div
      {...tabProps}
      ref={ref}
    >
      {rendered}
    </div>
  );
}

function TabPanel(
  { state, ...props }
) {
  let ref = React
    .useRef();
  let { tabPanelProps } =
    useTabPanel(
      props,
      state,
      ref
    );
  return (
    <div
      {...tabPanelProps}
      ref={ref}
    >
      {state.selectedItem
        ?.props.children}
    </div>
  );
}

<Tabs aria-label="History of Ancient Rome">
  <Item
    key="FoR"
    title="Founding of Rome"
  >
    Arma virumque cano,
    Troiae qui primus
    ab oris.
  </Item>
  <Item
    key="MaR"
    title="Monarchy and Republic"
  >
    Senatus Populusque
    Romanus.
  </Item>
  <Item
    key="Emp"
    title="Empire"
  >
    Alea jacta est.
  </Item>
</Tabs>
Show CSS
.tabs {
  height: 150px;
  display: flex;
  flex-direction: column;
}

.tabs.vertical {
  flex-direction: row;
}

[role=tablist] {
  display: flex;
}

[role=tablist][aria-orientation=horizontal] {
  border-bottom: 1px solid gray;
}

[role=tablist][aria-orientation=vertical] {
  flex-direction: column;
  border-right: 1px solid gray;
}

[role=tab] {
  padding: 10px;
}

[role=tablist][aria-orientation=horizontal] [role=tab] {
  border-bottom: 3px solid transparent;
}

[role=tablist][aria-orientation=vertical] [role=tab] {
  border-right: 3px solid transparent;
}

[role=tablist] [role=tab][aria-selected=true] {
  border-color: var(--blue);
}

[role=tab][aria-disabled] {
  opacity: 0.5;
}

[role=tabpanel] {
  padding: 10px;
}
.tabs {
  height: 150px;
  display: flex;
  flex-direction: column;
}

.tabs.vertical {
  flex-direction: row;
}

[role=tablist] {
  display: flex;
}

[role=tablist][aria-orientation=horizontal] {
  border-bottom: 1px solid gray;
}

[role=tablist][aria-orientation=vertical] {
  flex-direction: column;
  border-right: 1px solid gray;
}

[role=tab] {
  padding: 10px;
}

[role=tablist][aria-orientation=horizontal] [role=tab] {
  border-bottom: 3px solid transparent;
}

[role=tablist][aria-orientation=vertical] [role=tab] {
  border-right: 3px solid transparent;
}

[role=tablist] [role=tab][aria-selected=true] {
  border-color: var(--blue);
}

[role=tab][aria-disabled] {
  opacity: 0.5;
}

[role=tabpanel] {
  padding: 10px;
}
.tabs {
  height: 150px;
  display: flex;
  flex-direction: column;
}

.tabs.vertical {
  flex-direction: row;
}

[role=tablist] {
  display: flex;
}

[role=tablist][aria-orientation=horizontal] {
  border-bottom: 1px solid gray;
}

[role=tablist][aria-orientation=vertical] {
  flex-direction: column;
  border-right: 1px solid gray;
}

[role=tab] {
  padding: 10px;
}

[role=tablist][aria-orientation=horizontal] [role=tab] {
  border-bottom: 3px solid transparent;
}

[role=tablist][aria-orientation=vertical] [role=tab] {
  border-right: 3px solid transparent;
}

[role=tablist] [role=tab][aria-selected=true] {
  border-color: var(--blue);
}

[role=tab][aria-disabled] {
  opacity: 0.5;
}

[role=tabpanel] {
  padding: 10px;
}

Styled examples#


Animated Selection
A TabList component with an animated selection indicator.

Usage#


The following examples show how to use the Tabs component created in the above example.

Default selection#

A default selected tab can be provided using the defaultSelectedKey prop, which should correspond to the key prop provided to each item. When Tabs is used with dynamic items as described below, the key of each item is derived from the data. See the react-stately Selection docs for more details.

<Tabs aria-label="Input settings" defaultSelectedKey="keyboard">
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  defaultSelectedKey="keyboard"
>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  defaultSelectedKey="keyboard"
>
  <Item key="mouse">
    Mouse Settings
  </Item>
  <Item key="keyboard">
    Keyboard Settings
  </Item>
  <Item key="gamepad">
    Gamepad Settings
  </Item>
</Tabs>

Controlled selection#

Selection can be controlled using the selectedKey prop, paired with the onSelectionChange event. The key prop from the selected tab will be passed into the callback when the tab is selected, allowing you to update state accordingly.

function Example() {
  let [timePeriod, setTimePeriod] = React.useState('triassic');

  return (
    <>
      <p>Selected time period: {timePeriod}</p>
      <Tabs
        aria-label="Mesozoic time periods"
        selectedKey={timePeriod}
        onSelectionChange={setTimePeriod}
      >
        <Item key="triassic" title="Triassic">
          The Triassic ranges roughly from 252 million to 201 million years ago,
          preceding the Jurassic Period.
        </Item>
        <Item key="jurassic" title="Jurassic">
          The Jurassic ranges from 200 million years to 145 million years ago.
        </Item>
        <Item key="cretaceous" title="Cretaceous">
          The Cretaceous is the longest period of the Mesozoic, spanning from
          145 million to 66 years ago.
        </Item>
      </Tabs>
    </>
  );
}
function Example() {
  let [timePeriod, setTimePeriod] = React.useState(
    'triassic'
  );

  return (
    <>
      <p>Selected time period: {timePeriod}</p>
      <Tabs
        aria-label="Mesozoic time periods"
        selectedKey={timePeriod}
        onSelectionChange={setTimePeriod}
      >
        <Item key="triassic" title="Triassic">
          The Triassic ranges roughly from 252 million to
          201 million years ago, preceding the Jurassic
          Period.
        </Item>
        <Item key="jurassic" title="Jurassic">
          The Jurassic ranges from 200 million years to 145
          million years ago.
        </Item>
        <Item key="cretaceous" title="Cretaceous">
          The Cretaceous is the longest period of the
          Mesozoic, spanning from 145 million to 66 years
          ago.
        </Item>
      </Tabs>
    </>
  );
}
function Example() {
  let [
    timePeriod,
    setTimePeriod
  ] = React.useState(
    'triassic'
  );

  return (
    <>
      <p>
        Selected time
        period:{' '}
        {timePeriod}
      </p>
      <Tabs
        aria-label="Mesozoic time periods"
        selectedKey={timePeriod}
        onSelectionChange={setTimePeriod}
      >
        <Item
          key="triassic"
          title="Triassic"
        >
          The Triassic
          ranges roughly
          from 252
          million to 201
          million years
          ago, preceding
          the Jurassic
          Period.
        </Item>
        <Item
          key="jurassic"
          title="Jurassic"
        >
          The Jurassic
          ranges from 200
          million years
          to 145 million
          years ago.
        </Item>
        <Item
          key="cretaceous"
          title="Cretaceous"
        >
          The Cretaceous
          is the longest
          period of the
          Mesozoic,
          spanning from
          145 million to
          66 years ago.
        </Item>
      </Tabs>
    </>
  );
}

Focusable content#

When the tab panel doesn't contain any focusable content, the entire panel is given a tabIndex=0 so that the content can be navigated to with the keyboard. When the tab panel contains focusable content, such as a textfield, then the tabIndex is omitted because the content itself can receive focus.

This example uses the same Tabs component from above. Try navigating from the tabs to the content for each panel using the keyboard.

<Tabs aria-label="Notes app">
  <Item key="item1" title="Jane Doe">
    <label>Leave a note for Jane: <input type="text" /></label>
  </Item>
  <Item key="item2" title="John Doe">Senatus Populusque Romanus.</Item>
  <Item key="item3" title="Joe Bloggs">Alea jacta est.</Item>
</Tabs>
<Tabs aria-label="Notes app">
  <Item key="item1" title="Jane Doe">
    <label>
      Leave a note for Jane: <input type="text" />
    </label>
  </Item>
  <Item key="item2" title="John Doe">
    Senatus Populusque Romanus.
  </Item>
  <Item key="item3" title="Joe Bloggs">
    Alea jacta est.
  </Item>
</Tabs>
<Tabs aria-label="Notes app">
  <Item
    key="item1"
    title="Jane Doe"
  >
    <label>
      Leave a note for
      Jane:{' '}
      <input type="text" />
    </label>
  </Item>
  <Item
    key="item2"
    title="John Doe"
  >
    Senatus Populusque
    Romanus.
  </Item>
  <Item
    key="item3"
    title="Joe Bloggs"
  >
    Alea jacta est.
  </Item>
</Tabs>

Dynamic items#

The above examples have shown tabs with static items. The items prop can be used when creating tabs from a dynamic collection, for example when the user can add and remove tabs, or the tabs come from an external data source. The function passed as the children of the Tabs component is called for each item in the list, and returns an <Item> representing the tab.

Each item accepts a key prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and a key prop is not required. See Collection Components for more details.

function Example() {
  let [tabs, setTabs] = React.useState([
    {id: 1, title: 'Tab 1', content: 'Tab body 1'},
    {id: 2, title: 'Tab 2', content: 'Tab body 2'},
    {id: 3, title: 'Tab 3', content: 'Tab body 3'}
  ]);

  let addTab = () => {
    setTabs(tabs => [
      ...tabs,
      {
        id: tabs.length + 1,
        title: `Tab ${tabs.length + 1}`,
        content: `Tab Body ${tabs.length + 1}`
      }
    ]);
  };

  let removeTab = () => {
    if (tabs.length > 1) {
      setTabs(tabs => tabs.slice(0, -1));
    }
  };

  return (
    <>
      <button onClick={addTab}>Add tab</button>
      <button onClick={removeTab}>Remove tab</button>
      <Tabs aria-label="Dynamic tabs" items={tabs}>
        {item => <Item title={item.title}>{item.content}</Item>}
      </Tabs>
    </>
  );
}
function Example() {
  let [tabs, setTabs] = React.useState([
    { id: 1, title: 'Tab 1', content: 'Tab body 1' },
    { id: 2, title: 'Tab 2', content: 'Tab body 2' },
    { id: 3, title: 'Tab 3', content: 'Tab body 3' }
  ]);

  let addTab = () => {
    setTabs((tabs) => [
      ...tabs,
      {
        id: tabs.length + 1,
        title: `Tab ${tabs.length + 1}`,
        content: `Tab Body ${tabs.length + 1}`
      }
    ]);
  };

  let removeTab = () => {
    if (tabs.length > 1) {
      setTabs((tabs) => tabs.slice(0, -1));
    }
  };

  return (
    <>
      <button onClick={addTab}>Add tab</button>
      <button onClick={removeTab}>Remove tab</button>
      <Tabs aria-label="Dynamic tabs" items={tabs}>
        {(item) => (
          <Item title={item.title}>{item.content}</Item>
        )}
      </Tabs>
    </>
  );
}
function Example() {
  let [tabs, setTabs] =
    React.useState([
      {
        id: 1,
        title: 'Tab 1',
        content:
          'Tab body 1'
      },
      {
        id: 2,
        title: 'Tab 2',
        content:
          'Tab body 2'
      },
      {
        id: 3,
        title: 'Tab 3',
        content:
          'Tab body 3'
      }
    ]);

  let addTab = () => {
    setTabs((tabs) => [
      ...tabs,
      {
        id: tabs.length +
          1,
        title: `Tab ${
          tabs.length + 1
        }`,
        content:
          `Tab Body ${
            tabs.length +
            1
          }`
      }
    ]);
  };

  let removeTab = () => {
    if (
      tabs.length > 1
    ) {
      setTabs((tabs) =>
        tabs.slice(0, -1)
      );
    }
  };

  return (
    <>
      <button
        onClick={addTab}
      >
        Add tab
      </button>
      <button
        onClick={removeTab}
      >
        Remove tab
      </button>
      <Tabs
        aria-label="Dynamic tabs"
        items={tabs}
      >
        {(item) => (
          <Item
            title={item
              .title}
          >
            {item
              .content}
          </Item>
        )}
      </Tabs>
    </>
  );
}

Keyboard Activation#

By default, pressing the arrow keys while focus is on a Tab will switch selection to the adjacent Tab in that direction, updating the content displayed accordingly. If you would like to prevent selection change from happening automatically you can set the keyboardActivation prop to "manual". This will prevent tab selection from changing on arrow key press, requiring a subsequent Enter or Space key press to confirm tab selection.

<Tabs aria-label="Input settings" keyboardActivation="manual">
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  keyboardActivation="manual"
>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  keyboardActivation="manual"
>
  <Item key="mouse">
    Mouse Settings
  </Item>
  <Item key="keyboard">
    Keyboard Settings
  </Item>
  <Item key="gamepad">
    Gamepad Settings
  </Item>
</Tabs>

Orientation#

By default, tabs are horizontally oriented. The orientation prop can be set to vertical to change this. This affects keyboard navigation. You are responsible for styling your tabs accordingly.

<Tabs aria-label="Chat log orientation example" orientation="vertical">
  <Item key="item1" title="John Doe">
    There is no prior chat history with John Doe.
  </Item>
  <Item key="item2" title="Jane Doe">
    There is no prior chat history with Jane Doe.
  </Item>
  <Item key="item3" title="Joe Bloggs">
    There is no prior chat history with Joe Bloggs.
  </Item>
</Tabs>
<Tabs
  aria-label="Chat log orientation example"
  orientation="vertical"
>
  <Item key="item1" title="John Doe">
    There is no prior chat history with John Doe.
  </Item>
  <Item key="item2" title="Jane Doe">
    There is no prior chat history with Jane Doe.
  </Item>
  <Item key="item3" title="Joe Bloggs">
    There is no prior chat history with Joe Bloggs.
  </Item>
</Tabs>
<Tabs
  aria-label="Chat log orientation example"
  orientation="vertical"
>
  <Item
    key="item1"
    title="John Doe"
  >
    There is no prior
    chat history with
    John Doe.
  </Item>
  <Item
    key="item2"
    title="Jane Doe"
  >
    There is no prior
    chat history with
    Jane Doe.
  </Item>
  <Item
    key="item3"
    title="Joe Bloggs"
  >
    There is no prior
    chat history with
    Joe Bloggs.
  </Item>
</Tabs>

Disabled#

All tabs can be disabled using the isDisabled prop.

<Tabs aria-label="Input settings" isDisabled>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs aria-label="Input settings" isDisabled>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  isDisabled
>
  <Item key="mouse">
    Mouse Settings
  </Item>
  <Item key="keyboard">
    Keyboard Settings
  </Item>
  <Item key="gamepad">
    Gamepad Settings
  </Item>
</Tabs>

Disabled items#

Individual tabs can be disabled using the disabledKeys prop. Each key in this list corresponds with the key prop passed to the Item component, or automatically derived from the values passed to the items prop. See Collections for more details.

<Tabs aria-label="Input settings" disabledKeys={['gamepad']}>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  disabledKeys={['gamepad']}
>
  <Item key="mouse">Mouse Settings</Item>
  <Item key="keyboard">Keyboard Settings</Item>
  <Item key="gamepad">Gamepad Settings</Item>
</Tabs>
<Tabs
  aria-label="Input settings"
  disabledKeys={[
    'gamepad'
  ]}
>
  <Item key="mouse">
    Mouse Settings
  </Item>
  <Item key="keyboard">
    Keyboard Settings
  </Item>
  <Item key="gamepad">
    Gamepad Settings
  </Item>
</Tabs>

Internationalization#


useTabList handles some aspects of internationalization automatically. For example, keyboard navigation is automatically mirrored for right-to-left languages. You are responsible for localizing all tab labels and content.

RTL#

In right-to-left languages, the tablist should be mirrored. The first tab is furthest right and the last tab is furthest left. Ensure that your CSS accounts for this.