Tabs

The Tabs component lets users switch between related views without leaving the page. It supports two visual variants, three sizes, controlled and uncontrolled modes, and disabled tabs — all built on Headless UI's TabGroup with full keyboard navigation.


Installation

npm install @libretexts/davis-react

Basic Usage

import { Tabs } from '@libretexts/davis-react';

export default function Example() {
  return (
    <Tabs>
      <Tabs.List>
        <Tabs.Tab>Overview</Tabs.Tab>
        <Tabs.Tab>Features</Tabs.Tab>
        <Tabs.Tab>Pricing</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panels>
        <Tabs.Panel>Overview content</Tabs.Panel>
        <Tabs.Panel>Features content</Tabs.Panel>
        <Tabs.Panel>Pricing content</Tabs.Panel>
      </Tabs.Panels>
    </Tabs>
  );
}

Variants

Two variants control how the active tab is indicated.

{/* Line: underline indicator on the active tab (default) */}
<Tabs variant="line">
  <Tabs.List>
    <Tabs.Tab>Overview</Tabs.Tab>
    <Tabs.Tab>Features</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Overview content</Tabs.Panel>
    <Tabs.Panel>Features content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

{/* Pills: pill-shaped background on the active tab */}
<Tabs variant="pills">
  <Tabs.List>
    <Tabs.Tab>Overview</Tabs.Tab>
    <Tabs.Tab>Features</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Overview content</Tabs.Panel>
    <Tabs.Panel>Features content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

Pills Color

When using the pills variant, the color prop controls the active tab's color scheme.

<Tabs variant="pills" color="white">
  <Tabs.List>
    <Tabs.Tab>White pill (default)</Tabs.Tab>
    <Tabs.Tab>Another tab</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>White pill content</Tabs.Panel>
    <Tabs.Panel>Another panel</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

<Tabs variant="pills" color="primary">
  <Tabs.List>
    <Tabs.Tab>Primary pill</Tabs.Tab>
    <Tabs.Tab>Another tab</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Primary pill content</Tabs.Panel>
    <Tabs.Panel>Another panel</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

The color prop has no effect when variant="line".


Sizes

<Tabs size="sm">
  <Tabs.List>
    <Tabs.Tab>Small</Tabs.Tab>
    <Tabs.Tab>Tabs</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Small content</Tabs.Panel>
    <Tabs.Panel>More content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

<Tabs size="md">...</Tabs>

<Tabs size="lg">...</Tabs>

Disabled Tabs

Add the disabled prop to individual Tabs.Tab elements to prevent selection.

<Tabs variant="line" size="md">
  <Tabs.List>
    <Tabs.Tab>Overview</Tabs.Tab>
    <Tabs.Tab>Features</Tabs.Tab>
    <Tabs.Tab disabled>Disabled</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Overview content</Tabs.Panel>
    <Tabs.Panel>Features content</Tabs.Panel>
    <Tabs.Panel>Disabled content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

Controlled Usage

Control the selected tab externally using selectedIndex and onChange.

import { Tabs } from '@libretexts/davis-react';
import { useState } from 'react';

export default function ControlledTabs() {
  const [selected, setSelected] = useState(0);

  return (
    <>
      <p>Active tab index: {selected}</p>
      <Tabs selectedIndex={selected} onChange={setSelected}>
        <Tabs.List>
          <Tabs.Tab>Overview</Tabs.Tab>
          <Tabs.Tab>Features</Tabs.Tab>
          <Tabs.Tab>Pricing</Tabs.Tab>
        </Tabs.List>
        <Tabs.Panels>
          <Tabs.Panel>Overview content</Tabs.Panel>
          <Tabs.Panel>Features content</Tabs.Panel>
          <Tabs.Panel>Pricing content</Tabs.Panel>
        </Tabs.Panels>
      </Tabs>
    </>
  );
}

Use defaultIndex instead of selectedIndex if you only need to set the initial tab without managing state.


Props

Tabs

PropTypeDefaultDescription
variant'line' | 'pills''line'Visual style of the tab indicators
size'sm' | 'md' | 'lg''md'Controls tab padding and font size
color'white' | 'primary''white'Active pill color (pills variant only)
defaultIndexnumber0Initially selected tab (uncontrolled)
selectedIndexnumber-Currently selected tab (controlled)
onChange(index: number) => void-Called when the selected tab changes
classNamestring-Additional CSS classes

Tabs.Tab

PropTypeDefaultDescription
disabledbooleanfalsePrevents the tab from being selected
classNamestring-Additional CSS classes
childrenReactNode-Tab label content

Tabs.List, Tabs.Panels, Tabs.Panel

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReactNode-Child content

Accessibility

The Tabs component is built on Headless UI's TabGroup and implements the ARIA Tabs pattern:

  • Tabs.List renders with role="tablist"
  • Each Tabs.Tab renders with role="tab", aria-selected, and aria-controls pointing to its panel
  • Each Tabs.Panel renders with role="tabpanel" and aria-labelledby pointing to its tab
  • Left/Right arrow keys move focus between tabs
  • Home moves focus to the first tab; End moves to the last
  • Space or Enter selects the focused tab
  • Disabled tabs are skipped during keyboard navigation and receive aria-disabled="true"

Best practices:

  • Keep tab labels short — one or two words — so the tab list is easy to scan
  • Each tab panel should contain meaningfully distinct content; avoid using tabs for sequential steps (use a stepper instead)
  • Do not use tabs inside tabs; nested tab groups create confusing navigation hierarchies