beta

Tabs

Tabs organize content into multiple sections and allow users to navigate between them.

installyarn add react-aria-components
version1.0.0-beta.1
usageimport {Tabs} from 'react-aria-components'

Example#


import {Tabs, TabList, Tab, TabPanel} from 'react-aria-components';

<Tabs>
  <TabList aria-label="History of Ancient Rome">
    <Tab id="FoR">Founding of Rome</Tab>
    <Tab id="MaR">Monarchy and Republic</Tab>
    <Tab id="Emp">Empire</Tab>
  </TabList>
  <TabPanel id="FoR">
    Arma virumque cano, Troiae qui primus ab oris.
  </TabPanel>
  <TabPanel id="MaR">
    Senatus Populusque Romanus.
  </TabPanel>
  <TabPanel id="Emp">
    Alea jacta est.
  </TabPanel>
</Tabs>
import {
  Tab,
  TabList,
  TabPanel,
  Tabs
} from 'react-aria-components';

<Tabs>
  <TabList aria-label="History of Ancient Rome">
    <Tab id="FoR">Founding of Rome</Tab>
    <Tab id="MaR">Monarchy and Republic</Tab>
    <Tab id="Emp">Empire</Tab>
  </TabList>
  <TabPanel id="FoR">
    Arma virumque cano, Troiae qui primus ab oris.
  </TabPanel>
  <TabPanel id="MaR">
    Senatus Populusque Romanus.
  </TabPanel>
  <TabPanel id="Emp">
    Alea jacta est.
  </TabPanel>
</Tabs>
import {
  Tab,
  TabList,
  TabPanel,
  Tabs
} from 'react-aria-components';

<Tabs>
  <TabList aria-label="History of Ancient Rome">
    <Tab id="FoR">
      Founding of Rome
    </Tab>
    <Tab id="MaR">
      Monarchy and
      Republic
    </Tab>
    <Tab id="Emp">
      Empire
    </Tab>
  </TabList>
  <TabPanel id="FoR">
    Arma virumque cano,
    Troiae qui primus
    ab oris.
  </TabPanel>
  <TabPanel id="MaR">
    Senatus Populusque
    Romanus.
  </TabPanel>
  <TabPanel id="Emp">
    Alea jacta est.
  </TabPanel>
</Tabs>
Show CSS
.react-aria-Tabs {
  --highlight-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-700);
  --text-color-hover: var(--spectrum-global-color-gray-800);
  --text-color-selected: var(--spectrum-global-color-gray-900);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  display: flex;

  &[data-orientation=horizontal] {
    flex-direction: column;
  }

  &[data-orientation=vertical] {
    flex-direction: row;
  }
}

.react-aria-TabList {
  display: flex;

  &[data-orientation=horizontal] {
    border-bottom: 1px solid gray;

    .react-aria-Tab {
      border-bottom: 3px solid var(--border-color, transparent);
    }
  }

  &[data-orientation=vertical] {
    flex-direction: column;
    border-right: 1px solid gray;

    .react-aria-Tab {
      border-right: 3px solid var(--border-color, transparent);
    }
  }
}

.react-aria-Tab {
  padding: 10px;
  cursor: default;
  outline: none;
  position: relative;
  color: var(--text-color);
  transition: color 200ms;

  &[data-hovered],
  &[data-focused] {
    color: var(--text-color-hover);
  }

  &[data-selected] {
    --border-color: var(--highlight-color);
    color: var(--text-color-selected);
  }

  &[data-disabled] {
    color: var(--text-color-disabled);
    &[data-selected] {
      --border-color: var(--text-color-disabled);
    }
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 4px;
    border: 2px solid var(--highlight-color);
  }
}

.react-aria-TabPanel {
  margin-top: 4px;
  padding: 10px;
  border-radius: 4px;
  outline: none;

  &[data-focus-visible] {
    box-shadow: inset 0 0 0 2px var(--highlight-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Tabs {
    forced-color-adjust: none;
    color: CanvasText;

    --highlight-color: Highlight;
    --text-color: ButtonText;
    --text-color-hover: ButtonText;
    --text-color-selected: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-Tabs {
  --highlight-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-700);
  --text-color-hover: var(--spectrum-global-color-gray-800);
  --text-color-selected: var(--spectrum-global-color-gray-900);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  display: flex;

  &[data-orientation=horizontal] {
    flex-direction: column;
  }

  &[data-orientation=vertical] {
    flex-direction: row;
  }
}

.react-aria-TabList {
  display: flex;

  &[data-orientation=horizontal] {
    border-bottom: 1px solid gray;

    .react-aria-Tab {
      border-bottom: 3px solid var(--border-color, transparent);
    }
  }

  &[data-orientation=vertical] {
    flex-direction: column;
    border-right: 1px solid gray;

    .react-aria-Tab {
      border-right: 3px solid var(--border-color, transparent);
    }
  }
}

.react-aria-Tab {
  padding: 10px;
  cursor: default;
  outline: none;
  position: relative;
  color: var(--text-color);
  transition: color 200ms;

  &[data-hovered],
  &[data-focused] {
    color: var(--text-color-hover);
  }

  &[data-selected] {
    --border-color: var(--highlight-color);
    color: var(--text-color-selected);
  }

  &[data-disabled] {
    color: var(--text-color-disabled);
    &[data-selected] {
      --border-color: var(--text-color-disabled);
    }
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 4px;
    border: 2px solid var(--highlight-color);
  }
}

.react-aria-TabPanel {
  margin-top: 4px;
  padding: 10px;
  border-radius: 4px;
  outline: none;

  &[data-focus-visible] {
    box-shadow: inset 0 0 0 2px var(--highlight-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Tabs {
    forced-color-adjust: none;
    color: CanvasText;

    --highlight-color: Highlight;
    --text-color: ButtonText;
    --text-color-hover: ButtonText;
    --text-color-selected: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-Tabs {
  --highlight-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-700);
  --text-color-hover: var(--spectrum-global-color-gray-800);
  --text-color-selected: var(--spectrum-global-color-gray-900);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  display: flex;

  &[data-orientation=horizontal] {
    flex-direction: column;
  }

  &[data-orientation=vertical] {
    flex-direction: row;
  }
}

.react-aria-TabList {
  display: flex;

  &[data-orientation=horizontal] {
    border-bottom: 1px solid gray;

    .react-aria-Tab {
      border-bottom: 3px solid var(--border-color, transparent);
    }
  }

  &[data-orientation=vertical] {
    flex-direction: column;
    border-right: 1px solid gray;

    .react-aria-Tab {
      border-right: 3px solid var(--border-color, transparent);
    }
  }
}

.react-aria-Tab {
  padding: 10px;
  cursor: default;
  outline: none;
  position: relative;
  color: var(--text-color);
  transition: color 200ms;

  &[data-hovered],
  &[data-focused] {
    color: var(--text-color-hover);
  }

  &[data-selected] {
    --border-color: var(--highlight-color);
    color: var(--text-color-selected);
  }

  &[data-disabled] {
    color: var(--text-color-disabled);
    &[data-selected] {
      --border-color: var(--text-color-disabled);
    }
  }

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 4px;
    border: 2px solid var(--highlight-color);
  }
}

.react-aria-TabPanel {
  margin-top: 4px;
  padding: 10px;
  border-radius: 4px;
  outline: none;

  &[data-focus-visible] {
    box-shadow: inset 0 0 0 2px var(--highlight-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Tabs {
    forced-color-adjust: none;
    color: CanvasText;

    --highlight-color: Highlight;
    --text-color: ButtonText;
    --text-color-hover: ButtonText;
    --text-color-selected: ButtonText;
    --text-color-disabled: GrayText;
  }
}

Features#


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

  • Flexible – Support for both horizontal and vertical orientations, disabled tabs, customizable layout, and multiple keyboard activation modes.
  • Accessible – Follows the ARIA tabs pattern, automatically linking tabs and their associated tab panels semantically. The arrow keys can be used to navigate between tabs, and tab panels automatically become focusable when they don't contain any focusable children.
  • International – Keyboard navigation is automatically mirrored in right-to-left languages.
  • Styleable – Hover, press, keyboard focus, and selection states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes.

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.

import {Tabs, TabList, Tab, TabPanel} from 'react-aria-components';

<Tabs>
  <TabList>
    <Tab />
  </TabList>
  <TabPanel />
</Tabs>
import {
  Tab,
  TabList,
  TabPanel,
  Tabs
} from 'react-aria-components';

<Tabs>
  <TabList>
    <Tab />
  </TabList>
  <TabPanel />
</Tabs>
import {
  Tab,
  TabList,
  TabPanel,
  Tabs
} from 'react-aria-components';

<Tabs>
  <TabList>
    <Tab />
  </TabList>
  <TabPanel />
</Tabs>

Concepts#

Tabs makes use of the following concepts:

Collections
Defining collections of items, async loading, and updating items over time.
Selection
Interactions and data structures to represent selection.

Selection#


Default selection#

A default selected tab can be provided using the defaultSelectedKey prop, which should correspond to the id 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 defaultSelectedKey="keyboard">
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs defaultSelectedKey="keyboard">
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs defaultSelectedKey="keyboard">
  <TabList aria-label="Input settings">
    <Tab id="mouse">
      Mouse Settings
    </Tab>
    <Tab id="keyboard">
      Keyboard Settings
    </Tab>
    <Tab id="gamepad">
      Gamepad Settings
    </Tab>
  </TabList>
  <TabPanel id="mouse">
    Mouse Settings
  </TabPanel>
  <TabPanel id="keyboard">
    Keyboard Settings
  </TabPanel>
  <TabPanel id="gamepad">
    Gamepad Settings
  </TabPanel>
</Tabs>

Controlled selection#

Selection can be controlled using the selectedKey prop, paired with the onSelectionChange event. The id 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<React.Key>('triassic');

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

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

  return (
    <>
      <p>
        Selected time
        period:{' '}
        {timePeriod}
      </p>
      <Tabs
        selectedKey={timePeriod}
        onSelectionChange={setTimePeriod}
      >
        <TabList aria-label="Mesozoic time periods">
          <Tab id="triassic">
            Triassic
          </Tab>
          <Tab id="jurassic">
            Jurassic
          </Tab>
          <Tab id="cretaceous">
            Cretaceous
          </Tab>
        </TabList>
        <TabPanel id="triassic">
          The Triassic
          ranges roughly
          from 252
          million to 201
          million years
          ago, preceding
          the Jurassic
          Period.
        </TabPanel>
        <TabPanel id="jurassic">
          The Jurassic
          ranges from 200
          million years
          to 145 million
          years ago.
        </TabPanel>
        <TabPanel id="cretaceous">
          The Cretaceous
          is the longest
          period of the
          Mesozoic,
          spanning from
          145 million to
          66 million
          years ago.
        </TabPanel>
      </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 keyboardActivation="manual">
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs keyboardActivation="manual">
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs keyboardActivation="manual">
  <TabList aria-label="Input settings">
    <Tab id="mouse">
      Mouse Settings
    </Tab>
    <Tab id="keyboard">
      Keyboard Settings
    </Tab>
    <Tab id="gamepad">
      Gamepad Settings
    </Tab>
  </TabList>
  <TabPanel id="mouse">
    Mouse Settings
  </TabPanel>
  <TabPanel id="keyboard">
    Keyboard Settings
  </TabPanel>
  <TabPanel id="gamepad">
    Gamepad Settings
  </TabPanel>
</Tabs>

Content#


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>
  <TabList aria-label="Notes app">
    <Tab id="1">Jane Doe</Tab>
    <Tab id="2">John Doe</Tab>
    <Tab id="3">Joe Bloggs</Tab>
  </TabList>
  <TabPanel id="1">
    <label>Leave a note for Jane: <input type="text" /></label>
  </TabPanel>
  <TabPanel id="2">Senatus Populusque Romanus.</TabPanel>
  <TabPanel id="3">Alea jacta est.</TabPanel>
</Tabs>
<Tabs>
  <TabList aria-label="Notes app">
    <Tab id="1">Jane Doe</Tab>
    <Tab id="2">John Doe</Tab>
    <Tab id="3">Joe Bloggs</Tab>
  </TabList>
  <TabPanel id="1">
    <label>
      Leave a note for Jane: <input type="text" />
    </label>
  </TabPanel>
  <TabPanel id="2">Senatus Populusque Romanus.</TabPanel>
  <TabPanel id="3">Alea jacta est.</TabPanel>
</Tabs>
<Tabs>
  <TabList aria-label="Notes app">
    <Tab id="1">
      Jane Doe
    </Tab>
    <Tab id="2">
      John Doe
    </Tab>
    <Tab id="3">
      Joe Bloggs
    </Tab>
  </TabList>
  <TabPanel id="1">
    <label>
      Leave a note for
      Jane:{' '}
      <input type="text" />
    </label>
  </TabPanel>
  <TabPanel id="2">
    Senatus Populusque
    Romanus.
  </TabPanel>
  <TabPanel id="3">
    Alea jacta est.
  </TabPanel>
</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 TabList component is called for each item in the list, and returns an <Tab>. A function passed as the children of the Collection component returns a corresponding <TabPanel> for each tab.

Each item accepts an id 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 an id prop is not required. See Collection Components for more details.

import {Collection, Button} from 'react-aria-components';

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 (
    <Tabs>
      <div style={{display: 'flex'}}>
        <TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}>
          {item => <Tab>{item.title}</Tab>}
        </TabList>
        <div className="button-group">
          <Button onPress={addTab}>Add tab</Button>
          <Button onPress={removeTab}>Remove tab</Button>
        </div>
      </div>
      <Collection items={tabs}>
        {item => <TabPanel>{item.content}</TabPanel>}
      </Collection>
    </Tabs>
  );
}
import {Button, Collection} from 'react-aria-components';

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 (
    <Tabs>
      <div style={{ display: 'flex' }}>
        <TabList
          aria-label="Dynamic tabs"
          items={tabs}
          style={{ flex: 1 }}
        >
          {(item) => <Tab>{item.title}</Tab>}
        </TabList>
        <div className="button-group">
          <Button onPress={addTab}>Add tab</Button>
          <Button onPress={removeTab}>Remove tab</Button>
        </div>
      </div>
      <Collection items={tabs}>
        {(item) => <TabPanel>{item.content}</TabPanel>}
      </Collection>
    </Tabs>
  );
}
import {
  Button,
  Collection
} from 'react-aria-components';

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 (
    <Tabs>
      <div
        style={{
          display: 'flex'
        }}
      >
        <TabList
          aria-label="Dynamic tabs"
          items={tabs}
          style={{
            flex: 1
          }}
        >
          {(item) => (
            <Tab>
              {item
                .title}
            </Tab>
          )}
        </TabList>
        <div className="button-group">
          <Button
            onPress={addTab}
          >
            Add tab
          </Button>
          <Button
            onPress={removeTab}
          >
            Remove tab
          </Button>
        </div>
      </div>
      <Collection
        items={tabs}
      >
        {(item) => (
          <TabPanel>
            {item
              .content}
          </TabPanel>
        )}
      </Collection>
    </Tabs>
  );
}
Show CSS
.button-group {
  border-bottom: 1px solid gray;
  display: flex;
  align-items: center;
  gap: 8px;
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}
.button-group {
  border-bottom: 1px solid gray;
  display: flex;
  align-items: center;
  gap: 8px;
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}
.button-group {
  border-bottom: 1px solid gray;
  display: flex;
  align-items: center;
  gap: 8px;
}

.react-aria-Button {
  --border-color: var(--spectrum-alias-border-color);
  --border-color-pressed: var(--spectrum-alias-border-color-down);
  --background-color: var(--spectrum-global-color-gray-50);
  --background-color-pressed: var(--spectrum-global-color-gray-100);
  --text-color: var(--spectrum-alias-text-color);
  --focus-ring-color: slateblue;

  color: var(--text-color);
  background: var(--background-color);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  appearance: none;
  vertical-align: middle;
  font-size: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  text-decoration: none;

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--background-color-pressed);
    border-color: var(--border-color-pressed);
  }

  &[data-focus-visible] {
    border-color: var(--focus-ring-color);
    box-shadow: 0 0 0 1px var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Button {
    forced-color-adjust: none;
    --border-color: ButtonBorder;
    --border-color-pressed: ButtonBorder;
    --background-color: ButtonFace;
    --background-color-pressed: ButtonFace;
    --text-color: ButtonText;
    --focus-ring-color: Highlight;
  }
}

Orientation#


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

<Tabs orientation="vertical">
  <TabList aria-label="Chat log orientation example">
    <Tab id="1">John Doe</Tab>
    <Tab id="2">Jane Doe</Tab>
    <Tab id="3">Joe Bloggs</Tab>
  </TabList>
  <TabPanel id="1">There is no prior chat history with John Doe.</TabPanel>
  <TabPanel id="2">There is no prior chat history with Jane Doe.</TabPanel>
  <TabPanel id="3">There is no prior chat history with Joe Bloggs.</TabPanel>
</Tabs>
<Tabs orientation="vertical">
  <TabList aria-label="Chat log orientation example">
    <Tab id="1">John Doe</Tab>
    <Tab id="2">Jane Doe</Tab>
    <Tab id="3">Joe Bloggs</Tab>
  </TabList>
  <TabPanel id="1">
    There is no prior chat history with John Doe.
  </TabPanel>
  <TabPanel id="2">
    There is no prior chat history with Jane Doe.
  </TabPanel>
  <TabPanel id="3">
    There is no prior chat history with Joe Bloggs.
  </TabPanel>
</Tabs>
<Tabs orientation="vertical">
  <TabList aria-label="Chat log orientation example">
    <Tab id="1">
      John Doe
    </Tab>
    <Tab id="2">
      Jane Doe
    </Tab>
    <Tab id="3">
      Joe Bloggs
    </Tab>
  </TabList>
  <TabPanel id="1">
    There is no prior
    chat history with
    John Doe.
  </TabPanel>
  <TabPanel id="2">
    There is no prior
    chat history with
    Jane Doe.
  </TabPanel>
  <TabPanel id="3">
    There is no prior
    chat history with
    Joe Bloggs.
  </TabPanel>
</Tabs>

Disabled#


All tabs can be disabled using the isDisabled prop.

<Tabs isDisabled>
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs isDisabled>
  <TabList aria-label="Input settings">
    <Tab id="mouse">Mouse Settings</Tab>
    <Tab id="keyboard">Keyboard Settings</Tab>
    <Tab id="gamepad">Gamepad Settings</Tab>
  </TabList>
  <TabPanel id="mouse">Mouse Settings</TabPanel>
  <TabPanel id="keyboard">Keyboard Settings</TabPanel>
  <TabPanel id="gamepad">Gamepad Settings</TabPanel>
</Tabs>
<Tabs isDisabled>
  <TabList aria-label="Input settings">
    <Tab id="mouse">
      Mouse Settings
    </Tab>
    <Tab id="keyboard">
      Keyboard Settings
    </Tab>
    <Tab id="gamepad">
      Gamepad Settings
    </Tab>
  </TabList>
  <TabPanel id="mouse">
    Mouse Settings
  </TabPanel>
  <TabPanel id="keyboard">
    Keyboard Settings
  </TabPanel>
  <TabPanel id="gamepad">
    Gamepad Settings
  </TabPanel>
</Tabs>

Disabled items#

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

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

Tabs may be rendered as links to different routes in your application. This can be achieved by passing the href prop to the <Tab> component. By default, links perform native browser navigation. However, you'll usually want to synchronize the selected tab with the URL from your client side router. This takes two steps:

  1. Set up a RouterProvider at the root of your app. This will handle link navigation from all React Aria components using your framework or router. See the client side routing guide to learn how to set this up.
  2. Use the selectedKey prop to set the selected tab based on the URL, as described above.

This example uses React Router to setup routes for each tab and synchronize the selection with the URL.

import {BrowserRouter, Route, Routes, useLocation, useNavigate} from 'react-router-dom';
import {RouterProvider} from 'react-aria-components';

function AppTabs() {
  let { pathname } = useLocation();

  return (
    <Tabs selectedKey={pathname}>
      <TabList aria-label="Tabs">
        <Tab id="/" href="/">Home</Tab>
        <Tab id="/shared" href="/shared">Shared</Tab>
        <Tab id="/deleted" href="/deleted">Deleted</Tab>
      </TabList>
      <TabPanel id={pathname}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/shared" element={<SharedPage />} />
          <Route path="/deleted" element={<DeletedPage />} />
        </Routes>
      </TabPanel>
    </Tabs>
  );
}

function App() {
  let navigate = useNavigate();
  return (
    <RouterProvider navigate={navigate}>
      <Routes>
        <Route path="/*" element={<AppTabs />} />
      </Routes>
    </RouterProvider>
  );
}

<BrowserRouter>
  <App />
</BrowserRouter>
import {
  BrowserRouter,
  Route,
  Routes,
  useLocation,
  useNavigate
} from 'react-router-dom';
import {RouterProvider} from 'react-aria-components';

function AppTabs() {
  let { pathname } = useLocation();

  return (
    <Tabs selectedKey={pathname}>
      <TabList aria-label="Tabs">
        <Tab id="/" href="/">Home</Tab>
        <Tab id="/shared" href="/shared">Shared</Tab>
        <Tab id="/deleted" href="/deleted">Deleted</Tab>
      </TabList>
      <TabPanel id={pathname}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/shared" element={<SharedPage />} />
          <Route
            path="/deleted"
            element={<DeletedPage />}
          />
        </Routes>
      </TabPanel>
    </Tabs>
  );
}

function App() {
  let navigate = useNavigate();
  return (
    <RouterProvider navigate={navigate}>
      <Routes>
        <Route path="/*" element={<AppTabs />} />
      </Routes>
    </RouterProvider>
  );
}

<BrowserRouter>
  <App />
</BrowserRouter>
import {
  BrowserRouter,
  Route,
  Routes,
  useLocation,
  useNavigate
} from 'react-router-dom';
import {RouterProvider} from 'react-aria-components';

function AppTabs() {
  let { pathname } =
    useLocation();

  return (
    <Tabs
      selectedKey={pathname}
    >
      <TabList aria-label="Tabs">
        <Tab
          id="/"
          href="/"
        >
          Home
        </Tab>
        <Tab
          id="/shared"
          href="/shared"
        >
          Shared
        </Tab>
        <Tab
          id="/deleted"
          href="/deleted"
        >
          Deleted
        </Tab>
      </TabList>
      <TabPanel
        id={pathname}
      >
        <Routes>
          <Route
            path="/"
            element={
              <HomePage />
            }
          />
          <Route
            path="/shared"
            element={
              <SharedPage />
            }
          />
          <Route
            path="/deleted"
            element={
              <DeletedPage />
            }
          />
        </Routes>
      </TabPanel>
    </Tabs>
  );
}

function App() {
  let navigate =
    useNavigate();
  return (
    <RouterProvider
      navigate={navigate}
    >
      <Routes>
        <Route
          path="/*"
          element={
            <AppTabs />
          }
        />
      </Routes>
    </RouterProvider>
  );
}

<BrowserRouter>
  <App />
</BrowserRouter>

Props#


Tabs#

NameTypeDefaultDescription
isDisabledboolean

Whether the TabList is disabled. Shows that a selection exists, but is not available in that circumstance.

disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectedKeyKeynullThe currently selected key in the collection (controlled).
defaultSelectedKeyKeyThe initial selected key in the collection (uncontrolled).
keyboardActivation'automatic''manual''automatic'Whether tabs are activated automatically on focus or manually.
orientationOrientation'horizontal'The orientation of the tabs.
childrenReactNode( (values: TabsRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: TabsRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TabsRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDescription
onSelectionChange( (key: Key )) => anyHandler that is called when the selection changes.
Layout
NameTypeDescription
slotstringnull

A slot name for the component. Slots allow the component to receive props from a parent component. An explicit null value indicates that the local props completely override all props received from a parent.

Accessibility
NameTypeDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

TabList#

NameTypeDescription
classNamestring( (values: TabListRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TabListRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
childrenReactNode( (item: T )) => ReactNodeThe contents of the collection.
itemsIterable<T>Item objects in the collection.
Accessibility
NameTypeDescription
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Tab#

NameTypeDescription
childrenReactNode( (values: TabRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: TabRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TabRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
hrefstringA URL to link to. See MDN.
targetHTMLAttributeAnchorTargetThe target window for the link. See MDN.
relstringThe relationship between the linked resource and the current page. See MDN.
downloadbooleanstringCauses the browser to download the linked URL. A string may be provided to suggest a file name. See MDN.
pingstringA space-separated list of URLs to ping when the link is followed. See MDN.
referrerPolicyHTMLAttributeReferrerPolicyHow much of the referrer to send when following the link. See MDN.
Accessibility
NameTypeDescription
idKey
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

TabPanel#

NameTypeDefaultDescription
shouldForceMountbooleanfalse

Whether to mount the tab panel in the DOM even when it is not currently selected. Inactive tab panels are inert and cannot be interacted with. They must be styled appropriately so this is clear to the user visually.

childrenReactNode( (values: TabPanelRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: TabPanelRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TabPanelRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Accessibility
NameTypeDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-Tabs {
  /* ... */
}
.react-aria-Tabs {
  /* ... */
}
.react-aria-Tabs {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<Tabs className="my-tabs">
  {/* ... */}
</Tabs>
<Tabs className="my-tabs">
  {/* ... */}
</Tabs>
<Tabs className="my-tabs">
  {/* ... */}
</Tabs>

In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example:

.react-aria-Tab[data-selected] {
  /* ... */
}

.react-aria-Tab[data-focus-visible] {
  /* ... */
}
.react-aria-Tab[data-selected] {
  /* ... */
}

.react-aria-Tab[data-focus-visible] {
  /* ... */
}
.react-aria-Tab[data-selected] {
  /* ... */
}

.react-aria-Tab[data-focus-visible] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<Tab
  className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Settings
</Tab>
<Tab
  className={({ isSelected }) =>
    isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Settings
</Tab>
<Tab
  className={(
    { isSelected }
  ) =>
    isSelected
      ? 'bg-blue-400'
      : 'bg-gray-100'}
>
  Settings
</Tab>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when an item is selected.

<Tab>
  {({isSelected}) => (
    <>
      {isSelected && <SelectionIndicator />}
      Item
    </>
  )}
</Tab>
<Tab>
  {({isSelected}) => (
    <>
      {isSelected && <SelectionIndicator />}
      Item
    </>
  )}
</Tab>
<Tab>
  {(
    { isSelected }
  ) => (
    <>
      {isSelected && (
        <SelectionIndicator />
      )}
      Item
    </>
  )}
</Tab>

The states and selectors for each component used in Tabs are documented below.

Tabs#

Tabs can be targeted with the .react-aria-Tabs CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
orientation[data-orientation="horizontal | vertical"]The orientation of the tabs.

TabList#

A TabList can be targeted with the .react-aria-TabList CSS selector, or by overriding with a custom className. It supports the following states:

NameCSS SelectorDescription
orientation[data-orientation="horizontal | vertical"]The orientation of the tab list.
stateState of the tab list.

Tab#

A Tab can be targeted with the .react-aria-Tab CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
isHovered[data-hovered]Whether the tab is currently hovered with a mouse.
isPressed[data-pressed]Whether the tab is currently in a pressed state.
isSelected[data-selected]Whether the tab is currently selected.
isFocused[data-focused]Whether the tab is currently focused.
isFocusVisible[data-focus-visible]Whether the tab is currently keyboard focused.
isDisabled[data-disabled]Whether the tab is disabled.

TabPanel#

A TabPanel can be targeted with the .react-aria-TabPanel CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
isFocused[data-focused]Whether the tab panel is currently focused.
isFocusVisible[data-focus-visible]Whether the tab panel is currently keyboard focused.
isInert[data-inert]

Whether the tab panel is currently non-interactive. This occurs when the shouldForceMount prop is true, and the corresponding tab is not selected.

stateState of the tab list.

Advanced customization#


Contexts#

All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps).

ComponentContextPropsRef
TabsTabsContextTabsPropsHTMLDivElement

This example shows a Router component that accepts Tabs and Link elements as children. When a link is clicked, it updates the selected tab accordingly.

import type {PressEvent} from 'react-aria-components';
import {TabsContext, LinkContext} from 'react-aria-components';

function Router({children}) {
  let [selectedKey, onSelectionChange] = React.useState(null);
  let onPress = (e: PressEvent) => {
    onSelectionChange(e.target.getAttribute('data-href'));
  };

  return (
    <TabsContext.Provider value={{selectedKey, onSelectionChange}}>      <LinkContext.Provider value={{onPress}}>
        {children}
      </LinkContext.Provider>
    </TabsContext.Provider>
  );
}
import type {PressEvent} from 'react-aria-components';
import {
  LinkContext,
  TabsContext
} from 'react-aria-components';

function Router({ children }) {
  let [selectedKey, onSelectionChange] = React.useState(
    null
  );
  let onPress = (e: PressEvent) => {
    onSelectionChange(e.target.getAttribute('data-href'));
  };

  return (
    <TabsContext.Provider
      value={{ selectedKey, onSelectionChange }}
    >      <LinkContext.Provider value={{ onPress }}>
        {children}
      </LinkContext.Provider>
    </TabsContext.Provider>
  );
}
import type {PressEvent} from 'react-aria-components';
import {
  LinkContext,
  TabsContext
} from 'react-aria-components';

function Router(
  { children }
) {
  let [
    selectedKey,
    onSelectionChange
  ] = React.useState(
    null
  );
  let onPress = (
    e: PressEvent
  ) => {
    onSelectionChange(
      e.target
        .getAttribute(
          'data-href'
        )
    );
  };

  return (
    <TabsContext.Provider
      value={{
        selectedKey,
        onSelectionChange
      }}
    >      <LinkContext.Provider
        value={{
          onPress
        }}
      >
        {children}
      </LinkContext.Provider>
    </TabsContext.Provider>
  );
}

Now clicking a link rendered within a Router navigates to the linked tab.

import {Link} from 'react-aria-components';

<Router>
  <Tabs>
    <TabList aria-label="Mesozoic time periods">
      <Tab id="triassic">Triassic</Tab>
      <Tab id="jurassic">Jurassic</Tab>
      <Tab id="cretaceous">Cretaceous</Tab>
    </TabList>
    <TabPanel id="triassic">
      The Triassic ranges roughly from 252 million to 201 million years ago,
      preceding the <Link data-href="jurassic">Jurassic Period</Link>.
    </TabPanel>
    <TabPanel id="jurassic">
      The Jurassic ranges from 200 million years to 145 million years ago,
      preceding the <Link data-href="cretaceous">Cretaceous Period</Link>.
    </TabPanel>
    <TabPanel id="cretaceous">
      The Cretaceous is the longest period of the Mesozoic, spanning from 145
      million to 66 million years ago.
    </TabPanel>
  </Tabs>
</Router>
import {Link} from 'react-aria-components';

<Router>
  <Tabs>
    <TabList aria-label="Mesozoic time periods">
      <Tab id="triassic">Triassic</Tab>
      <Tab id="jurassic">Jurassic</Tab>
      <Tab id="cretaceous">Cretaceous</Tab>
    </TabList>
    <TabPanel id="triassic">
      The Triassic ranges roughly from 252 million to 201
      million years ago, preceding the{' '}
      <Link data-href="jurassic">Jurassic Period</Link>.
    </TabPanel>
    <TabPanel id="jurassic">
      The Jurassic ranges from 200 million years to 145
      million years ago, preceding the{' '}
      <Link data-href="cretaceous">
        Cretaceous Period
      </Link>.
    </TabPanel>
    <TabPanel id="cretaceous">
      The Cretaceous is the longest period of the
      Mesozoic, spanning from 145 million to 66 million
      years ago.
    </TabPanel>
  </Tabs>
</Router>
import {Link} from 'react-aria-components';

<Router>
  <Tabs>
    <TabList aria-label="Mesozoic time periods">
      <Tab id="triassic">
        Triassic
      </Tab>
      <Tab id="jurassic">
        Jurassic
      </Tab>
      <Tab id="cretaceous">
        Cretaceous
      </Tab>
    </TabList>
    <TabPanel id="triassic">
      The Triassic
      ranges roughly
      from 252 million
      to 201 million
      years ago,
      preceding the
      {' '}
      <Link data-href="jurassic">
        Jurassic Period
      </Link>.
    </TabPanel>
    <TabPanel id="jurassic">
      The Jurassic
      ranges from 200
      million years to
      145 million years
      ago, preceding
      the{' '}
      <Link data-href="cretaceous">
        Cretaceous
        Period
      </Link>.
    </TabPanel>
    <TabPanel id="cretaceous">
      The Cretaceous is
      the longest
      period of the
      Mesozoic,
      spanning from 145
      million to 66
      million years
      ago.
    </TabPanel>
  </Tabs>
</Router>
Show CSS
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-800);

  color: var(--text-color);
  outline: none;
  position: relative;
  text-decoration: underline;
  cursor: pointer;

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -2px -4px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Link {
    --focus-ring-color: Highlight;
    --text-color: LinkText;
  }
}
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-800);

  color: var(--text-color);
  outline: none;
  position: relative;
  text-decoration: underline;
  cursor: pointer;

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -2px -4px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Link {
    --focus-ring-color: Highlight;
    --text-color: LinkText;
  }
}
.react-aria-Link {
  --focus-ring-color: slateblue;
  --text-color: var(--spectrum-global-color-gray-800);

  color: var(--text-color);
  outline: none;
  position: relative;
  text-decoration: underline;
  cursor: pointer;

  &[data-focus-visible]:after {
    content: '';
    position: absolute;
    inset: -2px -4px;
    border-radius: 6px;
    border: 2px solid var(--focus-ring-color);
  }
}

@media (forced-colors: active) {
  .react-aria-Link {
    --focus-ring-color: Highlight;
    --text-color: LinkText;
  }
}

State#

Tabs provides a TabListState object to its children via TabListStateContext. This can be used to access and manipulate the tab list state.

This example shows a TabNavigation component that can be placed within Tabs to navigate to the previous or next selected tab.

import {TabListStateContext, Button} from 'react-aria-components';

function TabNavigation() {
  let state = React.useContext(TabListStateContext);  let prevKey = state?.collection.getKeyBefore(state.selectedKey);
  let nextKey = state?.collection.getKeyAfter(state.selectedKey);
  let onPrev = prevKey != null ? () => state.setSelectedKey(prevKey) : null;
  let onNext = nextKey != null ? () => state.setSelectedKey(nextKey) : null;
  return (
    <div className="button-group">
      <Button aria-label="Previous tab" onPress={onPrev}></Button>
      <Button aria-label="Next tab" onPress={onNext}></Button>
    </div>
  );
}

<Tabs>
  <div style={{display: 'flex'}}>
    <TabList aria-label="Tabs" style={{flex: 1}}>
      <Tab id="home">Home</Tab>
      <Tab id="projects">Projects</Tab>
      <Tab id="search">Search</Tab>
    </TabList>
    <TabNavigation />  </div>
  <TabPanel id="home">Home</TabPanel>
  <TabPanel id="projects">Projects</TabPanel>
  <TabPanel id="search">Search</TabPanel>
</Tabs>
import {
  Button,
  TabListStateContext
} from 'react-aria-components';

function TabNavigation() {
  let state = React.useContext(TabListStateContext);  let prevKey = state?.collection.getKeyBefore(
    state.selectedKey
  );
  let nextKey = state?.collection.getKeyAfter(
    state.selectedKey
  );
  let onPrev = prevKey != null
    ? () => state.setSelectedKey(prevKey)
    : null;
  let onNext = nextKey != null
    ? () => state.setSelectedKey(nextKey)
    : null;
  return (
    <div className="button-group">
      <Button aria-label="Previous tab" onPress={onPrev}></Button>
      <Button aria-label="Next tab" onPress={onNext}></Button>
    </div>
  );
}

<Tabs>
  <div style={{ display: 'flex' }}>
    <TabList aria-label="Tabs" style={{ flex: 1 }}>
      <Tab id="home">Home</Tab>
      <Tab id="projects">Projects</Tab>
      <Tab id="search">Search</Tab>
    </TabList>
    <TabNavigation />  </div>
  <TabPanel id="home">Home</TabPanel>
  <TabPanel id="projects">Projects</TabPanel>
  <TabPanel id="search">Search</TabPanel>
</Tabs>
import {
  Button,
  TabListStateContext
} from 'react-aria-components';

function TabNavigation() {
  let state = React
    .useContext(
      TabListStateContext
    );  let prevKey = state
    ?.collection
    .getKeyBefore(
      state.selectedKey
    );
  let nextKey = state
    ?.collection
    .getKeyAfter(
      state.selectedKey
    );
  let onPrev =
    prevKey != null
      ? () =>
        state
          .setSelectedKey(
            prevKey
          )
      : null;
  let onNext =
    nextKey != null
      ? () =>
        state
          .setSelectedKey(
            nextKey
          )
      : null;
  return (
    <div className="button-group">
      <Button
        aria-label="Previous tab"
        onPress={onPrev}
      ></Button>
      <Button
        aria-label="Next tab"
        onPress={onNext}
      ></Button>
    </div>
  );
}

<Tabs>
  <div
    style={{
      display: 'flex'
    }}
  >
    <TabList
      aria-label="Tabs"
      style={{
        flex: 1
      }}
    >
      <Tab id="home">
        Home
      </Tab>
      <Tab id="projects">
        Projects
      </Tab>
      <Tab id="search">
        Search
      </Tab>
    </TabList>
    <TabNavigation />  </div>
  <TabPanel id="home">
    Home
  </TabPanel>
  <TabPanel id="projects">
    Projects
  </TabPanel>
  <TabPanel id="search">
    Search
  </TabPanel>
</Tabs>

Hooks#

If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTabList for more details.