Tabs
- Source: https://github.com/reach/reach-ui/tree/main/packages/tabs
- WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel
An accessible tabs component.
The Tab
and TabPanel
elements are associated by their order in the tree. None of the components are empty wrappers, each is associated with a real DOM element in the document, giving you maximum control over styling and composition.
You can render any other elements you want inside of Tabs
, but TabList
should only render Tab
elements, and TabPanels
should only render TabPanel
elements.
function Example() { return ( <Tabs> <TabList> <Tab>One</Tab> <Tab>Two</Tab> <Tab>Three</Tab> </TabList> <TabPanels> <TabPanel> <p>one!</p> </TabPanel> <TabPanel> <p>two!</p> </TabPanel> <TabPanel> <p>three!</p> </TabPanel> </TabPanels> </Tabs> );}
Check out the demos for ideas on how to style and compose.
Installation
From the command line in your project directory, run npm install @reach/tabs
or yarn add @reach/tabs
. Then import the components and styles that you need:
npm install @reach/tabs# oryarn add @reach/tabs
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs";import "@reach/tabs/styles.css";
Component API
Tabs
The parent component of the tab interface.
Tabs CSS Selectors
Please see the styling guide.
[data-reach-tabs] {}
Tabs Props
Prop | Type | Required | Default |
---|---|---|---|
as | string | Component | false | "div" |
children | node | true | |
defaultIndex | number | false | |
index | number | false | |
keyboardActivation | "auto" | "manual" ) | false | "auto |
onChange | func | false | |
orientation | "horizontal" |"vertical" | false | "horizontal" |
div props | false |
Tabs as
as?: keyof JSX.IntrinsicElements | React.ComponentType
Tabs will render a div
unless you specify a different element.
Tabs children
children: React.ReactNode | ((props: { focusedIndex: number; id: string; selectedIndex: number }) => React.ReactNode)
Tabs expects <TabList>
and <TabPanels>
as children. The order doesn't matter; you can have tabs on the top or the bottom. You can have random elements inside as well.
function Example() { return ( <Tabs> <div>Random</div> <TabPanels> <TabPanel>Uno</TabPanel> <TabPanel>Dos</TabPanel> </TabPanels> <TabList> <Tab>Uno</Tab> <Tab>Dos</Tab> </TabList> </Tabs> );}
You can also pass a render function to access data relevant to nested components.
function Example() { return ( <Tabs keyboardActivation="manual"> {({ selectedIndex, focusedIndex }) => { let getTabStyle = (index) => ({ borderBottom: `4px solid ${ selectedIndex === index ? "red" : focusedIndex === index ? "blue" : "black" }`, }); return ( <React.Fragment> <TabList> <Tab style={getTabStyle(0)}>Uno</Tab> <Tab style={getTabStyle(1)}>Dos</Tab> <Tab style={getTabStyle(2)}>Tres</Tab> </TabList> <TabPanels> <TabPanel>Uno</TabPanel> <TabPanel>Dos</TabPanel> <TabPanel>Tres</TabPanel> </TabPanels> </React.Fragment> ); }} </Tabs> );}
Tabs defaultIndex
defaultIndex?: number
Starts the tabs at a specific index.
function Example() { return ( <Tabs defaultIndex={1}> <TabPanels> <TabPanel> <img src="https://placekitten.com/400/200" alt="A picture of a cat" /> </TabPanel> <TabPanel> <img src="https://www.placecage.com/400/200" alt="A picture of Nicolas Cage" /> </TabPanel> </TabPanels> <TabList> <Tab>Kitten</Tab> <Tab>Cage</Tab> </TabList> </Tabs> );}
Tabs index
index?: number
Like form inputs, a tab's state can be controlled by the owner. Make sure to include an onChange
as well, or else the tabs will not be interactive.
function Example() { const [tabIndex, setTabIndex] = React.useState(0); const handleSliderChange = (event) => { setTabIndex(parseInt(event.target.value, 10)); }; const handleTabsChange = (index) => { setTabIndex(index); }; return ( <div> <input type="range" min="0" max="2" value={tabIndex} onChange={handleSliderChange} /> <Tabs index={tabIndex} onChange={handleTabsChange}> <TabList> <Tab>One</Tab> <Tab>Two</Tab> <Tab>Three</Tab> </TabList> <TabPanels> <TabPanel> <p>Click the tabs or pull the slider around</p> </TabPanel> <TabPanel> <p>Yeah yeah. What's up?</p> </TabPanel> <TabPanel> <p>Oh, hello there.</p> </TabPanel> </TabPanels> </Tabs> </div> );}
Tabs keyboardActivation
keyboardActivation?: TabsKeyboardActivation
Describes the activation mode when navigating a tablist with a keyboard. When set to "auto"
(TabsKeyboardActivation.Auto
), a tab panel is activated automatically when a tab is highlighted using arrow keys. When set to "manual"
(TabsKeyboardActivation.Manual
), the user must activate the tab panel with either the Spacebar or Enter keys. Defaults to "auto"
.
NOTE: TypeScript users should import and use the
TabsKeyboardActivation
enum when used in strict mode.
import { TabsKeyboardActivation } from "@reach/tabs"; function MyTabs() { return ( <Tabs keyboardActivation={TabsKeyboardActivation.Manual}>{/* ... */}</Tabs> );}
Tabs onChange
onChange?: (index: number) => void
Calls back with the tab index whenever the user changes tabs, allowing your app to synchronize with it.
function Example() { const colors = ["firebrick", "goldenrod", "dodgerblue"]; const [tabIndex, setTabIndex] = React.useState(0); const backgroundColor = colors[tabIndex]; return ( <Tabs onChange={(index) => setTabIndex(index)} style={{ color: "white", background: backgroundColor, }} > <TabList> <Tab>Red</Tab> <Tab>Yellow</Tab> <Tab>Blue</Tab> </TabList> <TabPanels style={{ padding: 20 }}> <TabPanel>The Primary Colors</TabPanel> <TabPanel>Are 1, 2, 3</TabPanel> <TabPanel>Red, yellow and blue.</TabPanel> </TabPanels> </Tabs> );}
Tabs orientation
orientation?: TabsOrientation
Allows you to switch the orientation of the tabs relative to their tab panels. This value can either be "horizontal"
(TabsOrientation.Horizontal
) or "vertical"
(TabsOrientation.Vertical
). Defaults to "horizontal"
.
Changing the orientation will change how the arrow keys navigate between tabs. Arrow key navigation should logically follow the order in which tabs appear on the screen. For screen reader users, the aria-orientation
attribute provides the appropriate context to direct which keys should navigate to the next tab (this is provided automatically). As such, it's important to use this prop even if you have already styled your tabs for vertical layout.
function Example() { // Try changing the orientation! return ( <Tabs orientation="vertical"> <TabList> <Tab>One</Tab> <Tab>Two</Tab> <Tab>Three</Tab> </TabList> <TabPanels> <TabPanel> <p>one!</p> </TabPanel> <TabPanel> <p>two!</p> </TabPanel> <TabPanel> <p>three!</p> </TabPanel> </TabPanels> </Tabs> );}
NOTE: TypeScript users should import and use the
TabsOrientation
enum when used in strict mode.
import { TabsOrientation } from "@reach/tabs"; function MyTabs() { return <Tabs orientation={TabsOrientation.Vertical}>{/* ... */}</Tabs>;}
Tabs div props
All other props are passed to the underlying div
(or another component passed to the as
prop).
TabList
The parent component of the tabs.
<TabList> <Tab>Tacos</Tab> <Tab>Tortas</Tab></TabList>
TabList CSS Selectors
Please see the styling guide.
[data-reach-tab-list] {}
TabList Props
TabList children
children: React.ReactNode
TabList
expects multiple Tab
elements as children.
<TabList> <Tab>One</Tab> <Tab>Two</Tab></TabList>
But, you can also wrap Tab
as long as you forward the props (because data is passed from TabList
to Tab
via React context).
const RedTab = (props) => <Tab {...props} style={{ color: "red" }} />; const TabPage = () => ( <Tabs> <TabList> <RedTab>This is red</RedTab> <Tab>This is normal</Tab> </TabList> <TabPanels> <TabPanel>...</TabPanel> <TabPanel>...</TabPanel> </TabPanels> </Tabs>);
TabList as
as?: keyof JSX.IntrinsicElements | React.ComponentType
Tabs will render a div
unless you specify a different element.
<TabList as={View} />
TabList div props
All other props are passed to the underlying div
(or component passed to as
).
TabPanels
The parent component of the panels.
<TabPanels> <TabPanel>My favorite</TabPanel> <TabPanel>My other favorite</TabPanel></TabPanels>
TabPanels CSS Selectors
Please see the styling guide.
[data-reach-tab-panels] {}
TabPanels Props
TabPanels as
as?: keyof JSX.IntrinsicElements | React.ComponentType
Tabs will render a div
unless you specify a different element.
<TabPanels as={View} />
TabPanels children
children: React.ReactNode
TabPanels
expects multiple TabPanel
elements as children.
<TabPanels> <TabPanel>One</TabPanel> <TabPanel>Two</TabPanel></TabPanels>
But, you can also wrap TabPanel
as long as you forward the props (because data is passed from TabPanels
to TabPanel
via React context`).
const BoldPanel = (props) => ( <TabPanel {...props} style={{ fontWeight: "bold" }} />); const TabPage = () => ( <Tabs> <TabList> <Tab>...</Tab> <Tab>...</Tab> </TabList> <TabPanels> <BoldPanel>...</BoldPanel> <TabPanel>...</TabPanel> </TabPanels> </Tabs>);
TabPanels div props
All other props are passed to the underlying div
(or component passed to as
).
Tab
The interactive element that changes the selected panel.
<Tab>Coconut Korma</Tab>
Tab CSS Selectors
Please see the styling guide.
/* styles all tabs */[data-reach-tab] {} /* styles only the selected tab */[data-reach-tab][data-selected] {}
Tab Props
Prop | Type | Required | Default |
---|---|---|---|
as | string | Component | false | "button" |
children | node | true | |
disabled | boolean | false | false |
button props |
Tab as
as?: keyof JSX.IntrinsicElements | React.ComponentType
Tab will render a button
unless you specify a different element.
<Tab as={ReactNativeWebButton} />
Tab children
children: React.ReactNode
Tab
can receive any type of children.
<Tab> <HouseIcon /> Home</Tab>
Tab disabled
disabled?: boolean
Disables a tab when true. Clicking will not work and keyboard navigation will skip over it.
<Tab disabled />
Tab button props
All other props are passed to the underlying button
(or component passed to as
).
Tab isSelected
Because TabList needs to know the order of the children, we use cloneElement
to pass state internally. If you want to know if a tab is active, you can wrap it, and then inspect clone props passed in.
NOTE: We will deprecate this behavior in the future. We now prefer to use context to pass data down to descendants for
Tabs
for better composability. If you'd like your Tabs to be a bit more future proof, we suggest using a controlledTabs
component so that your app knows the state of your tabs.
(() => { function CoolTab(props) { // `isSelected` comes from `TabList` cloning the `CoolTab`. const { isSelected, children } = props; // make sure to forward *all* props received from TabList return ( <Tab {...props}> {isSelected ? "😎" : "😐"} {children} </Tab> ); } return ( <Tabs> <TabList> <CoolTab>One</CoolTab> <CoolTab>Two</CoolTab> </TabList> <TabPanels> <TabPanel>1</TabPanel> <TabPanel>2</TabPanel> </TabPanels> </Tabs> );})();
TabPanel
The panel that displays when it's corresponding tab is active.
<TabPanel> <h2>The Best Food</h2> <p>The best food is either Mexican or Indian.</p></TabPanel>
TabPanel CSS Selectors
Please see the styling guide.
/* styles all tabs */[data-reach-tab-panel] {}
TabPanel Props
TabPanel as
as?: keyof JSX.IntrinsicElements | React.ComponentType
TabPanel will render a div
unless you specify a different element.
<Tab as={View} />
TabPanel children
children?: React.ReactNode
TabPanel
can receive any type of children.
<TabPanel> <h2>Whatever you want</h2> <p>In here</p></TabPanel>
TabPanel div props
All other props are passed to the underlying div
(or component passed to as
).
useTabsContext
function useTabsContext(): { focusedIndex: number; id: string; selectedIndex: number }
A hook that exposes data for a given Tabs
component to its descendants.
(() => { function CustomTab({ index, ...props }) { const { selectedIndex, focusedIndex } = useTabsContext(); return ( <Tab style={{ borderBottom: `4px solid ${ selectedIndex === index ? "red" : focusedIndex === index ? "blue" : "black" }`, }} {...props} /> ); } return ( <Tabs keyboardActivation="manual"> <TabList> <CustomTab index={0}>Uno</CustomTab> <CustomTab index={1}>Dos</CustomTab> <CustomTab index={2}>Tres</CustomTab> </TabList> <TabPanels> <TabPanel> <p>Uno</p> </TabPanel> <TabPanel> <p>Dos</p> </TabPanel> <TabPanel> <p>Tres</p> </TabPanel> </TabPanels> </Tabs> );})();
Demos
These demos show off how you can add quite a bit of behavior to your Tabs interfaces.
Data-driven tabs
If you'd like to drive your tabs with data you can create a DataTabs
component.
(() => { function DataTabs({ data }) { return ( <Tabs> <TabList> {data.map((tab, index) => ( <Tab key={index}>{tab.label}</Tab> ))} </TabList> <TabPanels> {data.map((tab, index) => ( <TabPanel key={index}>{tab.content}</TabPanel> ))} </TabPanels> </Tabs> ); } // now if you have an array of data... const tabData = [ { label: "Taco", content: "Perhaps the greatest dish ever invented." }, { label: "Burrito", content: "Perhaps the greatest dish ever invented but bigger and with rice.", }, ]; // you can just pass it in: return <DataTabs data={tabData} />;})();
Animation
With a little composition we can animate the selected tab bar.
(() => { const HORIZONTAL_PADDING = 8; const AnimatedContext = React.createContext(); function AnimatedTabs({ color, children, ...rest }) { // some state to store the position we want to animate to const [activeRect, setActiveRect] = React.useState(null); const ref = React.useRef(); const rect = useRect(ref); return ( // put the function to change the styles on context so an active Tab // can call it, then style it up <AnimatedContext.Provider value={setActiveRect}> {/* make sure to forward props since we're wrapping Tabs */} <Tabs {...rest} ref={ref} style={{ ...rest.style, position: "relative" }} > <div style={{ position: "absolute", height: 2, background: color, transition: "all 300ms ease", left: (activeRect && activeRect.left) - (rect && rect.left) + HORIZONTAL_PADDING, top: (activeRect && activeRect.bottom) - (rect && rect.top), // subtract both sides of horizontal padding to center the div width: activeRect && activeRect.width - HORIZONTAL_PADDING * 2, }} /> {children} </Tabs> </AnimatedContext.Provider> ); } function AnimatedTab({ index, ...props }) { // get the currently selected index from useTabsContext const { selectedIndex } = useTabsContext(); const isSelected = selectedIndex === index; // measure the size of our element, only listen to rect if active const ref = React.useRef(); const rect = useRect(ref, { observe: isSelected }); // get the style changing function from context const setActiveRect = useContext(AnimatedContext); // callup to set styles whenever we're active React.useLayoutEffect(() => { if (isSelected) { setActiveRect(rect); } }, [isSelected, rect, setActiveRect]); return ( <Tab ref={ref} {...props} style={{ ...props.style, border: "none", padding: `4px ${HORIZONTAL_PADDING}px`, }} /> ); } return ( <AnimatedTabs color="red" style={{ width: 400 }}> <TabList style={{ justifyContent: "space-around" }}> <AnimatedTab index={0} style={{ flex: 1 }}> The First </AnimatedTab> <AnimatedTab index={1} style={{ flex: 2 }}> This has longer text </AnimatedTab> <AnimatedTab index={2} style={{ flex: 1 }}> Three </AnimatedTab> </TabList> <TabPanels style={{ padding: 10 }}> <TabPanel> <p>Check it out! It's ~animated~</p> </TabPanel> <TabPanel> <p>Yeah yeah. What's up?</p> </TabPanel> <TabPanel> <p>Oh, hello there.</p> </TabPanel> </TabPanels> </AnimatedTabs> );})();
Keyboard Accessibility
Key | Action |
---|---|
Enter / Spacebar | Sets the focused tab to active when keyboardActivation is set to "manual" . |
ArrowUp / ArrowDown | Navigates between tabs when orientation is "vertical" . |
ArrowLeft / ArrowRight | Navigates between tabs when orientation is "horizontal" . |
Home / PageUp | Navigates to the last tab in the TabList . |
End / PageDown | Navigates to the first tab in the TabList . |