Docs
Tabs

Tabs

Set of tabs that organizes content into switchable panels. A controlled component: the consumer keeps selectedIndex in state and is notified via onChange when a different tab is clicked.

Each tab can have its own color (bottom indicator), arbitrary content (ReactNode) and an optional handleClick callback fired before onChange — useful for event tracking.

When to use

✅ Use when…🚫 Avoid when…
  • To switch between related views of the same resource (profile → activity → settings).
  • On detail screens where splitting the content vertically creates excessive scroll.
  • Primary product navigation. Use Menu or SideBarLayout — tabs are for locally scoped content.
  • When the user needs to compare panels side by side. Use grid/columns.
  • More than ~6 tabs. Consider grouping or switching to dropdown selection.

Basic usage

Content of the first tab.
import { Tabs } from '@apollion-dsi/core/data-display/tabs';
import { bell } from '@apollion-dsi/core/icons';
 
const Example = () => {
  const [index, setIndex] = React.useState(0);
 
  return (
    <Tabs
      selectedIndex={index}
      onChange={setIndex}
      tabs={[
        { tabTitle: 'First Tab', content: <FirstPage /> },
        { tabTitle: 'Second Tab', content: <SecondPage /> },
        { tabTitle: 'Third Tab', content: <ThirdPage /> },
      ]}
    />
  );
};

Changing the bar color

The bottom indicator color follows the theme palette and is set per tab.

Warning indicator.
<Tabs
  tabs={[
    { tabTitle: 'First', content: <Page1 />, color: 'warning' },
    { tabTitle: 'Second', content: <Page2 />, color: 'danger' },
  ]}
/>

Indicator

The selected-tab indicator is a 2px segment on the header rule, painted with the selected tab's color (primary by default). It is a dedicated pseudo-element, not a border-bottom, so linked buttons and layout props cannot zero it; inactive tabs keep the neutral hairline only.

Always active

To keep a tab's indicator always on, use isActive:

<Tabs
  tabs={[
    {
      tabTitle: 'First',
      content: <Page1 />,
    },
    {
      isActive: true,
      color: 'warning',
      tabTitle: 'Promo',
      content: <Promo />,
    },
  ]}
/>

Custom title

tabTitle accepts any ReactNode — combine icons, badges and tooltips if the tab needs more signaling:

<Tabs
  tabs={[
    {
      tabTitle: (
        <Flex flexDirection="row" wrap="wrap" gap="micro" alignItems="center">
          <Icon icon={bell} size="small" />
          <Text>Notifications</Text>
        </Flex>
      ),
      content: <NotificationsPanel />,
    },
  ]}
/>

Accessibility

Follows the ARIA Tabs pattern (opens in a new tab) with no extra configuration.

KeyAction
TabEnters and leaves the tab set as one stop — not one per tab
/ Moves between tabs, wrapping at the ends
Home / EndFirst / last tab

The header is a tablist, each button is a tab with aria-selected, and the content is a tabpanel labeled by the active tab. Focus follows selection.

Do not override role or tabIndex via buttonProps: it breaks keyboard navigation.

Properties

Prop
Type
Default
Description
color
string
Theme token (`theme.colors`) used as `color`. Has lower precedence than `contrast`: when both are provided, the contrast calculation wins. Stays `string` (not the typed union): `color` is also an HTML attribute (`<li color>`, etc.) and interfaces extending native props require identical types on the name collision.
containment
string
Containment marker for the `cq` channel: emits `container-type` (+ `container-name` when the string form carries one, e.g. `'inline-size card'`). Apply on the immediate wrapper of the adapting component — NEVER on page-level shells.
legibility
"on-photo"
Reading-shadow preset for text over a photographic background (`on-photo`). Replaces the inline `style={{ textShadow }}` in the consumer (brasil_2030 radar, gap A3). Token emission is deferred until a 2nd consumer asks for the raw var (see backlog).
onChange *
(index: number) => void
Fired when clicking a tab other than the current one.
pageShell
boolean
Centers the element and caps the width at `theme.layout.pageMaxWidth`. The page shell of a classic centered layout.
readable
number | boolean
Makes the `color` legible against the page surface: `true` = WCAG AA (4.5), a number sets a custom floor. Ignored with `contrast`. See the Layout Props concept page for the full semantics.
selectedIndex
number
0
Index of the active tab.
tabContentProps
DefaultFlexInterface
Extra `Flex` props applied to the active tab's content container.
tabHeaderProps
DefaultFlexInterface
Extra `Flex` props applied to the tabs header.
tabs *
TabType[]
Tab definitions (title + content + options).
transform
string
CSS `transform` value (e.g. `translate(-50%, -33px)`). Never leaks to the DOM as an attribute.

Beyond the props above, every component accepts the layout props (spacing, color, flex/grid, sizing, border) — not repeated here.

See also

  • Storybook story: Components / Tabs