Black Lives Matter.  Support the Equal Justice Initiative.
Skip to main content
Reakit
DocumentationNewsletter
GitHub
GitHub

Tab

Accessible Tab component that follows the WAI-ARIA Tabs Pattern. It's a component that, when activated, display a TabPanel.

#Installation

npm install reakit

Learn more in Get started.

#Usage

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState();
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab} disabled>
          Tab 2
        </Tab>
        <Tab {...tab}>Tab 3</Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab}>Tab 2</TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </>
  );
}

#Default selected tab

You can set the default selected tab by passing an id to selectedId on useTabState.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState({ selectedId: "tab3" });
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab} disabled>
          Tab 2
        </Tab>
        <Tab {...tab} id="tab3">
          Tab 3
        </Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab}>Tab 2</TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </>
  );
}

Note that Tab automatically generates an id if you don't specify one. In this example, an id is explicitly assigned for the third tab, so that the same value can be specified as the selectedId. No id is assigned for the other tabs. The automatically generated ids are fine, since we don't need to reference them.

#No selected tab

By default, the first tab will be selected, but you can unset it by passing null to selectedId on useTabState.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState({ selectedId: null });
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab}>Tab 2</Tab>
        <Tab {...tab}>Tab 3</Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab}>Tab 2</TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </>
  );
}

#Manual activation

By default, a Tab is selected when it gets focused, which reveals its corresponding TabPanel. This behavior can be changed by setting manual to true on useTabState.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState({ manual: true });
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab}>Tab 2</Tab>
        <Tab {...tab}>Tab 3</Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab}>Tab 2</TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </>
  );
}

#Detecting the selected tab

On each render, selectedId from useTabState specifies which tab is currently selected. You can check it and alter your render accordingly. For example, since only the panel for the selected tab is visible, you can avoid rendering the contents of the other ones as an optimization.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState();
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab} id="tab1">
          Tab 1
        </Tab>
        <Tab {...tab} id="tab2">
          Tab 2
        </Tab>
        <Tab {...tab} id="tab3">
          Tab 3
        </Tab>
      </TabList>
      <TabPanel {...tab}>{tab.selectedId === "tab1" && "Tab 1"}</TabPanel>
      <TabPanel {...tab}>{tab.selectedId === "tab2" && "Tab 2"}</TabPanel>
      <TabPanel {...tab}>{tab.selectedId === "tab3" && "Tab 3"}</TabPanel>
    </>
  );
}

#Non-tabbable tab panels

By default, TabPanels are tabbable. You can disable this by passing a tabIndex prop to the TabPanel: either -1 to make it not tababble but still focusable, or undefined to make it neither tabbable nor focusable.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";
import { Button } from "reakit/Button";

function Example() {
  const tab = useTabState();
  return (
    <>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab}>Tab 2</Tab>
        <Tab {...tab}>Tab 3</Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab} tabIndex={undefined}>
        <Button>Button</Button>
      </TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </>
  );
}

You should only do this if the contents of the TabPanel are tabbable.

#Vertical tabs

You can control the orientation of the tabs by setting orientation on useTabState. Since it composes from CompositeItem, explicitly defining an orientation will change how arrow key navigation works. If it's set to vertical, only and will work.

import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";

function Example() {
  const tab = useTabState({ orientation: "vertical" });
  return (
    <div style={{ display: "flex" }}>
      <TabList {...tab} aria-label="My tabs">
        <Tab {...tab}>Tab 1</Tab>
        <Tab {...tab}>Tab 2</Tab>
        <Tab {...tab}>Tab 3</Tab>
      </TabList>
      <TabPanel {...tab}>Tab 1</TabPanel>
      <TabPanel {...tab}>Tab 2</TabPanel>
      <TabPanel {...tab}>Tab 3</TabPanel>
    </div>
  );
}

#Abstracting

Like all other Reakit components, you can leverage the low level API to create your own customized API and make it less verbose, for example, by using React Context underneath.

import React from "react";
import {
  useTabState,
  Tab as BaseTab,
  TabList as BaseTabList,
  TabPanel as BaseTabPanel,
} from "reakit/Tab";

const TabsContext = React.createContext();

function Tabs({ children, ...initialState }) {
  const tab = useTabState(initialState);
  const value = React.useMemo(() => tab, Object.values(tab));
  return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
}

function Tab(props) {
  const tab = React.useContext(TabsContext);
  return <BaseTab {...tab} {...props} />;
}

function TabList(props) {
  const tab = React.useContext(TabsContext);
  return <BaseTabList {...tab} {...props} />;
}

function TabPanel(props) {
  const tab = React.useContext(TabsContext);
  return <BaseTabPanel {...tab} {...props} />;
}

function Example() {
  return (
    <Tabs selectedId="tab3">
      <TabList aria-label="My tabs">
        <Tab>Tab 1</Tab>
        <Tab>Tab 2</Tab>
        <Tab id="tab3">Tab 3</Tab>
      </TabList>
      <TabPanel>Tab 1</TabPanel>
      <TabPanel>Tab 2</TabPanel>
      <TabPanel>Tab 3</TabPanel>
    </Tabs>
  );
}

#Accessibility

  • Tab has role tab.
  • Tab has aria-controls referring to its associated TabPanel.
  • The selected Tab has aria-selected set to true and all other Tabs have it set to false.
  • Tab extends the accessibility features of CompositeItem.
  • TabList has role tablist.
  • TabList has aria-orientation set to vertical or horizontal based on the value of the orientation option.
  • TabList extends the accessibility features of Composite.
  • TabPanel has role tabpanel.
  • TabPanel has aria-labelledby referring to its associated Tab.
  • TabPanel extends the accessibility features of DisclosureContent.

Learn more in Accessibility.

#Composition

Learn more in Composition.

#Props

#useTabState

  • baseId string

    ID that will serve as a base for all the items IDs.

  • unstable_virtual boolean

    If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.

  • rtl boolean

    Determines how next and previous functions will behave. If rtl is set to true, they will be inverted. You still need to set dir="rtl" on HTML/CSS.

  • orientation "horizontal" | "vertical" | undefined

    Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

    • undefined: all arrow keys work.
    • horizontal: only left and right arrow keys work.
    • vertical: only up and down arrow keys work.

    It doesn't have any effect on two-dimensional composites.

  • currentId string | null | undefined

    The current focused item id.

    • undefined will automatically focus the first enabled composite item.
    • null will focus the composite container and users will be able to navigate out of it using arrow keys.
    • If currentId is initially set to null, the composite element itself will have focus and users will be able to navigate to it using arrow keys.
  • loop boolean | "horizontal" | "vertical"

    On one-dimensional composites:

    • true loops from the last item to the first item and vice-versa.
    • horizontal loops only if orientation is horizontal or not set.
    • vertical loops only if orientation is vertical or not set.
    • If currentId is initially set to null, the composite element will be focused in between the last and first items.

    On two-dimensional composites:

    • true loops from the last row/column item to the first item in the same row/column and vice-versa. If it's the last item in the last row, it moves to the first item in the first row and vice-versa.
    • horizontal loops only from the last row item to the first item in the same row.
    • vertical loops only from the last column item to the first item in the column row.
    • If currentId is initially set to null, vertical loop will have no effect as moving down from the last row or up from the first row will focus the composite element.
    • If wrap matches the value of loop, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
  • wrap boolean | "horizontal" | "vertical"

    If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.

    • true wraps between rows and columns.
    • horizontal wraps only between rows.
    • vertical wraps only between columns.
    • If loop matches the value of wrap, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
  • selectedId string | null | undefined

    The current selected tab's id.

  • manual boolean

    Whether the tab selection should be manual.

#Tab

  • disabled boolean | undefined

    Same as the HTML attribute.

  • focusable boolean | undefined

    When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set.

  • id string | undefined

    Same as the HTML attribute.

19 state props

These props are returned by the state hook. You can spread them into this component ({...state}) or pass them separately. You can also provide these props from your own state logic.

  • baseId string

    ID that will serve as a base for all the items IDs.

  • unstable_virtual boolean

    If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.

  • orientation "horizontal" | "vertical" | undefined

    Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

    • undefined: all arrow keys work.
    • horizontal: only left and right arrow keys work.
    • vertical: only up and down arrow keys work.

    It doesn't have any effect on two-dimensional composites.

  • unstable_moves number

    Stores the number of moves that have been performed by calling move, next, previous, up, down, first or last.

  • currentId string | null | undefined

    The current focused item id.

    • undefined will automatically focus the first enabled composite item.
    • null will focus the composite container and users will be able to navigate out of it using arrow keys.
    • If currentId is initially set to null, the composite element itself will have focus and users will be able to navigate to it using arrow keys.
  • items Item[]

    Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

  • setCurrentId (value: SetStateAction<string | null | undefine...

    Sets currentId.

  • registerItem (item: Item) => void

    Registers a composite item.

  • unregisterItem (id: string) => void

    Unregisters a composite item.

  • next (unstable_allTheWay?: boolean | undefined) => void

    Moves focus to the next item.

  • previous (unstable_allTheWay?: boolean | undefined) => void

    Moves focus to the previous item.

  • up (unstable_allTheWay?: boolean | undefined) => void

    Moves focus to the item above.

  • down (unstable_allTheWay?: boolean | undefined) => void

    Moves focus to the item below.

  • first () => void

    Moves focus to the first item.

  • last () => void

    Moves focus to the last item.

  • manual boolean

    Whether the tab selection should be manual.

  • selectedId string | null | undefined

    The current selected tab's id.

  • panels Item[]

    Lists all the panels.

  • select (id: string | null) => void

    Moves into and selects a tab by its id.

#TabList

  • disabled boolean | undefined

    Same as the HTML attribute.

  • focusable boolean | undefined

    When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set.

12 state props

These props are returned by the state hook. You can spread them into this component ({...state}) or pass them separately. You can also provide these props from your own state logic.

  • baseId string

    ID that will serve as a base for all the items IDs.

  • unstable_virtual boolean

    If enabled, the composite element will act as an aria-activedescendant container instead of roving tabindex. DOM focus will remain on the composite while its items receive virtual focus.

  • orientation "horizontal" | "vertical" | undefined

    Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

    • undefined: all arrow keys work.
    • horizontal: only left and right arrow keys work.
    • vertical: only up and down arrow keys work.

    It doesn't have any effect on two-dimensional composites.

  • currentId string | null | undefined

    The current focused item id.

    • undefined will automatically focus the first enabled composite item.
    • null will focus the composite container and users will be able to navigate out of it using arrow keys.
    • If currentId is initially set to null, the composite element itself will have focus and users will be able to navigate to it using arrow keys.
  • wrap boolean | "horizontal" | "vertical"

    If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.

    • true wraps between rows and columns.
    • horizontal wraps only between rows.
    • vertical wraps only between columns.
    • If loop matches the value of wrap, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
  • unstable_moves number

    Stores the number of moves that have been performed by calling move, next, previous, up, down, first or last.

  • groups Group[]

    Lists all the composite groups with their id and DOM ref. This state is automatically updated when registerGroup and unregisterGroup are called.

  • items Item[]

    Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

  • move (id: string | null) => void

    Moves focus to a given item ID.

  • setCurrentId (value: SetStateAction<string | null | undefine...

    Sets currentId.

  • first () => void

    Moves focus to the first item.

  • last () => void

    Moves focus to the last item.

#TabPanel

  • id string | undefined

    Same as the HTML attribute.

  • tabId string | undefined

    Tab's id

10 state props

These props are returned by the state hook. You can spread them into this component ({...state}) or pass them separately. You can also provide these props from your own state logic.

  • baseId string

    ID that will serve as a base for all the items IDs.

  • visible boolean

    Whether it's visible or not.

  • animating boolean

    Whether it's animating or not.

  • animated number | boolean

    If true, animating will be set to true when visible is updated. It'll wait for stopAnimation to be called or a CSS transition ends. If animated is set to a number, stopAnimation will be called only after the same number of milliseconds have passed.

  • stopAnimation () => void

    Stops animation. It's called automatically if there's a CSS transition.

  • selectedId string | null | undefined

    The current selected tab's id.

  • items Item[]

    Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

  • panels Item[]

    Lists all the panels.

  • registerPanel (item: Item) => void

    Registers a tab panel.

  • unregisterPanel (id: string) => void

    Unregisters a tab panel.