static/app/components/core/tabs/tabs.mdx
import {Fragment, useState} from 'react';
import {TabList, TabPanels, Tabs} from '@sentry/scraps/tabs';
import * as Storybook from 'sentry/stories';
export const documentation = import('!!type-loader!@sentry/scraps/tabs');
<Tabs> is a compound component for creating tabbed interfaces. Built on React Aria, it provides full keyboard navigation, accessibility features, and automatic overflow handling out of the box.
Use <Tabs> to organize related content into separate views that users can switch between without leaving the page.
<Tabs>
<TabList>
<TabList.Item key="tab1">Tab 1</TabList.Item>
<TabList.Item key="tab2">Tab 2</TabList.Item>
</TabList>
<TabPanels>
<TabPanels.Item key="tab1">Content for Tab 1</TabPanels.Item>
<TabPanels.Item key="tab2">Content for Tab 2</TabPanels.Item>
</TabPanels>
</Tabs>
<Tabs> uses a compound component pattern with four main parts:
<Tabs>: Root component that manages state and provides context<TabList>: Container for tab buttons<TabList.Item>: Individual tab button<TabPanels>: Container for tab content panels<TabPanels.Item>: Individual content panel[!NOTE] All tab content is rendered upfront, including React hooks. The tabs system manages visibility, not mounting/unmounting. This ensures state is preserved when switching tabs.
The simplest tabs implementation with automatic state management:
export function BasicDemo() { const TABS = [ {key: 'one', label: 'Label 1', content: 'This is the first panel.'}, {key: 'two', label: 'Label 2', content: 'This is the second panel.'}, {key: 'three', label: 'Label 3', content: 'This is the third panel.'}, ]; return ( <Tabs> <TabList> {TABS.map(tab => ( <TabList.Item key={tab.key}>{tab.label}</TabList.Item> ))} </TabList> <TabPanels> {TABS.map(tab => ( <TabPanels.Item key={tab.key}>{tab.content}</TabPanels.Item> ))} </TabPanels> </Tabs> ); }
<Storybook.Demo> <BasicDemo /> </Storybook.Demo>
<Tabs>
<TabList>
<TabList.Item key="one">Label 1</TabList.Item>
<TabList.Item key="two">Label 2</TabList.Item>
<TabList.Item key="three">Label 3</TabList.Item>
</TabList>
<TabPanels>
<TabPanels.Item key="one">Content 1</TabPanels.Item>
<TabPanels.Item key="two">Content 2</TabPanels.Item>
<TabPanels.Item key="three">Content 3</TabPanels.Item>
</TabPanels>
</Tabs>
Set defaultValue to specify which tab is selected initially (uncontrolled):
<Storybook.Demo> <Tabs defaultValue="two"> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2 (default)</TabList.Item> <TabList.Item key="three">Tab 3</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Content 1</TabPanels.Item> <TabPanels.Item key="two">Content 2 (initially visible)</TabPanels.Item> <TabPanels.Item key="three">Content 3</TabPanels.Item> </TabPanels> </Tabs> </Storybook.Demo>
<Tabs defaultValue="two">
<TabList>
<TabList.Item key="two">Tab 2</TabList.Item>
</TabList>
</Tabs>
For controlled tabs, use value and onChange to manage the active tab externally:
export function ControlledDemo() { const [selected, setSelected] = useState('two'); return ( <Fragment> <p>Selected: {selected}</p> <Tabs value={selected} onChange={setSelected}> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2</TabList.Item> <TabList.Item key="three">Tab 3</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Content 1</TabPanels.Item> <TabPanels.Item key="two">Content 2</TabPanels.Item> <TabPanels.Item key="three">Content 3</TabPanels.Item> </TabPanels> </Tabs> </Fragment> ); }
<Storybook.Demo> <ControlledDemo /> </Storybook.Demo>
const [selected, setSelected] = useState('one');
<Tabs value={selected} onChange={setSelected}>
</Tabs>;
Disable all tabs with the disabled prop on <Tabs>, or disable individual tabs with disabled on <TabList.Item>:
<Storybook.Demo> <Storybook.SideBySide> <div> <p>All tabs disabled</p> <Tabs disabled> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Content 1</TabPanels.Item> <TabPanels.Item key="two">Content 2</TabPanels.Item> </TabPanels> </Tabs> </div> <div> <p>One tab disabled</p> <Tabs> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two" disabled> Tab 2 (disabled) </TabList.Item> <TabList.Item key="three">Tab 3</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Content 1</TabPanels.Item> <TabPanels.Item key="two">Content 2</TabPanels.Item> <TabPanels.Item key="three">Content 3</TabPanels.Item> </TabPanels> </Tabs> </div> </Storybook.SideBySide> </Storybook.Demo>
// Disable all tabs
<Tabs disabled>
</Tabs>
// Disable individual tab
<TabList.Item key="two" disabled>
Disabled Tab
</TabList.Item>
Tabs come in two visual variants: flat (default) and floating. The variant is set on the <TabList>:
<Storybook.Demo> <Tabs> <TabList variant="floating"> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2</TabList.Item> <TabList.Item key="three">Tab 3</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Floating variant content 1</TabPanels.Item> <TabPanels.Item key="two">Floating variant content 2</TabPanels.Item> <TabPanels.Item key="three">Floating variant content 3</TabPanels.Item> </TabPanels> </Tabs> </Storybook.Demo>
<TabList variant="flat"></TabList>
<TabList variant="floating"></TabList>
Tabs support both horizontal and vertical orientation:
<Storybook.Demo> <Storybook.SideBySide> <Tabs orientation="horizontal"> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Horizontal content 1</TabPanels.Item> <TabPanels.Item key="two">Horizontal content 2</TabPanels.Item> </TabPanels> </Tabs> <Tabs orientation="vertical"> <TabList> <TabList.Item key="one">Tab 1</TabList.Item> <TabList.Item key="two">Tab 2</TabList.Item> </TabList> <TabPanels> <TabPanels.Item key="one">Vertical content 1</TabPanels.Item> <TabPanels.Item key="two">Vertical content 2</TabPanels.Item> </TabPanels> </Tabs> </Storybook.SideBySide> </Storybook.Demo>
<Tabs orientation="horizontal"></Tabs>
<Tabs orientation="vertical"></Tabs>
When there are too many tabs to fit, they automatically overflow into a dropdown menu. This behavior is built-in and requires no configuration.
<Tabs>
<TabList>
{Array.from({length: 20}, (_, i) => (
<TabList.Item key={i}>Tab {i + 1}</TabList.Item>
))}
</TabList>
</Tabs>
To disable overflow (forcing horizontal scroll instead), use disableOverflow:
<Tabs disableOverflow></Tabs>
<Tabs> provides full keyboard support automatically:
Use tabs to organize related content into distinct views:
<Tabs>
<TabList>
<TabList.Item key="overview">Overview</TabList.Item>
<TabList.Item key="activity">Activity</TabList.Item>
<TabList.Item key="settings">Settings</TabList.Item>
</TabList>
<TabPanels>
<TabPanels.Item key="overview">
<OverviewContent />
</TabPanels.Item>
<TabPanels.Item key="activity">
<ActivityFeed />
</TabPanels.Item>
<TabPanels.Item key="settings">
<SettingsForm />
</TabPanels.Item>
</TabPanels>
</Tabs>
Sync tabs with URL parameters for shareable links:
const [tab, setTab] = useState(queryParams.tab || 'overview');
useEffect(() => {
// Update URL when tab changes
router.push({pathname, query: {...query, tab}});
}, [tab]);
<Tabs value={tab} onChange={setTab}>
</Tabs>;
<Tabs> is built with React Aria and automatically meets WCAG 2.2 AA standards:
The component automatically includes:
role="tablist", role="tab", and role="tabpanel"aria-selected state managementaria-controls linking tabs to panelsaria-labelledby linking panels to tabsMeaningful Tab Labels
// Good: Descriptive labels
<TabList.Item key="overview">Overview</TabList.Item>
<TabList.Item key="activity">Activity Feed</TabList.Item>
// Bad: Generic labels
<TabList.Item key="tab1">Tab 1</TabList.Item>
Content Organization
State Preservation
For more information, see the WAI-ARIA Tabs practices.