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/tabs
version3.0.2
usageimport {useTabList, useTab, useTabPanel} from '@react-aria/tabs'

API#


useTabList<T>( props: AriaTabListProps<T>, state: TabListState<T>, ref: RefObject<HTMLElement> ): TabListAriauseTab<T>( props: AriaTabProps, state: TabListState<T>, ref: RefObject<HTMLElement> ): TabAriauseTabPanel<T>( props: AriaTabPanelProps, state: TabListState<T>, ref: RefObject<HTMLElement> ): 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#


Tabs anatomy diagramSection 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
tabListPropsHTMLAttributes<HTMLElement>Props for the tablist container.

useTab returns props to be spread onto each individual tab:

NameTypeDescription
tabPropsHTMLAttributes<HTMLElement>Props for the tab element.

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

NameTypeDescription
tabPanelPropsHTMLAttributes<HTMLElement>Props 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.

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 {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';

function Tabs(props) {
  let state = useTabListState(props);
  let ref = React.useRef();
  let {tabListProps} = useTabList(props, state, ref);
  return (
    <div style={{height: '150px'}}>
      <div
        {...tabListProps}
        ref={ref}
        style={{display: 'flex', borderBottom: '1px solid grey'}}>
        {[...state.collection].map((item) => (
          <Tab key={item.key} item={item} state={state} />
        ))}
      </div>
      <TabPanel key={state.selectedItem?.key} state={state} />
    </div>
  );
}

function Tab({item, state}) {
  let {key, rendered} = item;
  let ref = React.useRef();
  let {tabProps} = useTab({key}, state, ref);
  let isSelected = state.selectedKey === key;
  let isDisabled = state.disabledKeys.has(key);
  return (
    <div
      {...tabProps}
      ref={ref}
      style={{
        padding: '10px',
        borderBottom: isSelected ? '3px solid var(--blue)' : undefined,
        opacity: isDisabled ? '0.5' : undefined
      }}>
      {item.rendered}
    </div>
  );
}

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

<Tabs aria-label="History of Ancient Rome" disabledKeys={['Emp']}>
  <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 {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';

function Tabs(props) {
  let state = useTabListState(props);
  let ref = React.useRef();
  let {tabListProps} = useTabList(props, state, ref);
  return (
    <div style={{height: '150px'}}>
      <div
        {...tabListProps}
        ref={ref}
        style={{
          display: 'flex',
          borderBottom: '1px solid grey'
        }}>
        {[...state.collection].map((item) => (
          <Tab key={item.key} item={item} state={state} />
        ))}
      </div>
      <TabPanel
        key={state.selectedItem?.key}
        state={state}
      />
    </div>
  );
}

function Tab({item, state}) {
  let {key, rendered} = item;
  let ref = React.useRef();
  let {tabProps} = useTab({key}, state, ref);
  let isSelected = state.selectedKey === key;
  let isDisabled = state.disabledKeys.has(key);
  return (
    <div
      {...tabProps}
      ref={ref}
      style={{
        padding: '10px',
        borderBottom: isSelected
          ? '3px solid var(--blue)'
          : undefined,
        opacity: isDisabled ? '0.5' : undefined
      }}>
      {item.rendered}
    </div>
  );
}

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

<Tabs
  aria-label="History of Ancient Rome"
  disabledKeys={['Emp']}>
  <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 {Item} from '@react-stately/collections';
import {useFocus} from '@react-aria/interactions';
import {mergeProps} from '@react-aria/utils';

function Tabs(props) {
  let state = useTabListState(
    props
  );
  let ref = React.useRef();
  let {
    tabListProps
  } = useTabList(
    props,
    state,
    ref
  );
  return (
    <div
      style={{
        height: '150px'
      }}>
      <div
        {...tabListProps}
        ref={ref}
        style={{
          display:
            'flex',
          borderBottom:
            '1px solid grey'
        }}>
        {[
          ...state.collection
        ].map((item) => (
          <Tab
            key={
              item.key
            }
            item={item}
            state={state}
          />
        ))}
      </div>
      <TabPanel
        key={
          state
            .selectedItem
            ?.key
        }
        state={state}
      />
    </div>
  );
}

function Tab({
  item,
  state
}) {
  let {
    key,
    rendered
  } = item;
  let ref = React.useRef();
  let {
    tabProps
  } = useTab(
    {key},
    state,
    ref
  );
  let isSelected =
    state.selectedKey ===
    key;
  let isDisabled = state.disabledKeys.has(
    key
  );
  return (
    <div
      {...tabProps}
      ref={ref}
      style={{
        padding: '10px',
        borderBottom: isSelected
          ? '3px solid var(--blue)'
          : undefined,
        opacity: isDisabled
          ? '0.5'
          : undefined
      }}>
      {item.rendered}
    </div>
  );
}

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

<Tabs
  aria-label="History of Ancient Rome"
  disabledKeys={[
    'Emp'
  ]}>
  <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>

With 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>

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.