---
title: Tabs
description: A component to display content in multiple tabs
---

```tsx
import { Tab, TabList, TabPanel, Tabs, TabsBody } from '../ui/tabs/Tabs';

export function TabsDemo() {
  const tabs = new Array(24).fill(undefined).map((_, n) => ({ label: `${n + 1}`, value: n + 1 }));

  const bgs = (count: number) => {
    switch (count % 3) {
      case 0:
        return 'bg-lightSand';
      case 1:
        return 'bg-saffron';
      case 2:
        return 'bg-lavender';
      default:
        return 'bg-sienna';
    }
  };
  return (
    <Tabs selected={1} className="max-w-500">
      <TabList>
        {tabs.map((tab) => {
          return <Tab key={tab.label} label={`Tab - ${tab.label}`} value={tab.value} />;
        })}
      </TabList>
      <TabsBody>
        {tabs.map((tab) => {
          return (
            <TabPanel key={tab.label} value={tab.value}>
              <div className={`p-16 ${bgs(tab.value)}`}>Panel - {tab.label}</div>
            </TabPanel>
          );
        })}
      </TabsBody>
    </Tabs>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/arrows.json
```

Or manually, copy the following code into `/ui/arrows.tsx`:

```tsx
import clsx from 'clsx';
import type { ComponentProps, ComponentPropsWithoutRef, FunctionComponent } from 'react';

import type { ArrowsLabels } from './ArrowsLabels';
// TODO: We need to migrate ArrowButton
import { ArrowButton } from '../buttons/ArrowButton';

export interface ArrowsProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
  color?: ComponentProps<typeof ArrowButton>['color'];
  describedby?: string;
  iconType?: 'ArrowDefault' | 'ArrowTail';
  isDisabledHidden?: boolean;
  isNextDisabled?: boolean;
  isPrevDisabled?: boolean;
  labels?: ArrowsLabels;
  onNext: ComponentProps<typeof ArrowButton>['onClick'];
  onPrev: ComponentProps<typeof ArrowButton>['onClick'];
  size?: ComponentProps<typeof ArrowButton>['size'];
  theme?: ComponentProps<typeof ArrowButton>['theme'];
}

export const Arrows: FunctionComponent<ArrowsProps> = ({
  className,
  color,
  describedby,
  iconType = 'ArrowDefault',
  isDisabledHidden = false,
  isNextDisabled = false,
  isPrevDisabled = false,
  labels = {
    next: '',
    previous: '',
  },
  onNext,
  onPrev,
  size,
  theme,
  ...attrs
}) => {
  return (
    <div
      {...attrs}
      className={clsx(
        'pointer-events-none flex w-full items-center justify-between gap-x-20',
        className,
      )}
      data-name="Arrows"
    >
      <ArrowButton
        className={clsx('pointer-events-auto transition-opacity', {
          'opacity-0': isDisabledHidden && isPrevDisabled,
        })}
        color={color}
        disabled={isPrevDisabled}
        icon={`${iconType}Left`}
        onClick={onPrev}
        size={size}
        theme={theme}
        aria-describedby={describedby}
      >
        {labels.previous}
      </ArrowButton>
      <ArrowButton
        className={clsx('pointer-events-auto transition-opacity', {
          'opacity-0': isDisabledHidden && isNextDisabled,
        })}
        color={color}
        disabled={isNextDisabled}
        icon={`${iconType}Right`}
        onClick={onNext}
        size={size}
        theme={theme}
        aria-describedby={describedby}
      >
        {labels.next}
      </ArrowButton>
    </div>
  );
};
```

## Usage

```tsx showLineNumbers
import { Tab, TabList, TabPanel, Tabs, TabsBody } from '../ui/tabs/Tabs';

export default function TabsDemo() {
  const tabs = new Array(3).fill(undefined).map((_, n) => ({ label: `${n + 1}`, value: n + 1 }));

  const bgs = (count: number) => {
    switch (count % 3) {
      case 0:
        return 'bg-lightSand';
      case 1:
        return 'bg-saffron';
      case 2:
        return 'bg-lavender';
      default:
        return 'bg-sienna';
    }
  };
  return (
    <Tabs selected={1}>
      <TabList>
        {tabs.map((tab) => {
          return <Tab key={tab.label} label={`Tab - ${tab.label}`} value={tab.value} />;
        })}
      </TabList>
      <TabsBody>
        {tabs.map((tab) => {
          return (
            <TabPanel key={tab.label} value={tab.value}>
              <div className={`p-16 ${bgs(tab.value)}`}>Panel - {tab.label}</div>
            </TabPanel>
          );
        })}
      </TabsBody>
    </Tabs>
  );
}
```

## API Reference

### Main Tabs component

Main container of the component, orchestrates

| Prop        | Type      | Default     | Description                                                        |
| ----------- | --------- | ----------- | ------------------------------------------------------------------ |
| `selected`  | number    | `undefined` | Default selected tab                                               |
| `children`  | children  | `undefined` |                                                                    |
| `compacted` | `boolean` | `false`     | Tightens the vertical spacing between the TabList and the TabsBody |
| `className` | `string`  | `undefined` | Additional className for the tabs container                        |

### Tab List component

Displays the Tab components side by side. When the tab list overflows its container, left and right scroll buttons appear automatically. The buttons are keyboard-accessible (Space/Enter to scroll) and use `aria-hidden` / `tabindex="-1"` when scrolling in that direction is not possible.

| Prop          | Type      | Default     | Description                                                       |
| ------------- | --------- | ----------- | ----------------------------------------------------------------- |
| `constrained` | `boolean` | `false`     | Whether or not to remove the spacers on the side of the tabs list |
| `className`   | `string`  | `undefined` | Additional className for the scrollable element                   |

### Tab component

The actual tab, clicking it will switch the current active tab and display its content in the TabPanel

| Prop           | Type                                              | Default     | Description                                                                        |
| -------------- | ------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------- |
| `label`        | `string`                                          | `undefined` | Tab Heading text                                                                   |
| `value`        | `number`                                          | _MANDATORY_ | Tab Heading index (can be 0 or 1 indexed)                                          |
| `notification` | `ReactNode`                                       | `undefined` | Badge content rendered inside the tab label (e.g. a count or `"!"`)               |
| `onSelect`     | `(context: {value:number, label:string}) => void` | `undefined` | Callback on tab selection                                                          |
| `as`           | `"div" \| "h1" \| "h2" \| "h3" \| "h4" \| "h5";`  | `h2`        | HTML tag to render the tab with                                                    |
| `className`    | `string`                                          | `undefined` | Additional className for the root element in this component                        |

### Tabs Body component

Container for the TabPanel components

| Prop        | Type     | Default     | Description                                                 |
| ----------- | -------- | ----------- | ----------------------------------------------------------- |
| `className` | `string` | `undefined` | Additional className for the root element in this component |

### Tab Panel component

Container for the content displayed when a tab is selected

| Prop        | Type                                | Default     | Description                                                 |
| ----------- | ----------------------------------- | ----------- | ----------------------------------------------------------- |
| `value`     | `number`                            | _MANDATORY_ | Index of the tab this panel is associated with              |
| `className` | `string`                            | `undefined` | Additional className for the root element in this component |
| `onSelect`  | `(context: {value:number}) => void` | `undefined` | Callback on tab panel selection                             |

## Examples

### Notification badge

Pass any `ReactNode` to the `notification` prop on a `Tab` to render a small badge next to the label. A number, a string like `"!"`, or any inline element works. The badge colour adapts on hover and focus so it always stays legible against the active-pill background.

```tsx
import { Tab, TabList, TabPanel, Tabs, TabsBody } from '../ui/tabs/Tabs';

export function TabsNotificationDemo() {
  return (
    <Tabs selected={1}>
      <TabList constrained>
        <Tab label="Overview" value={1} />
        <Tab label="Messages" value={2} notification={3} />
        <Tab label="Alerts" value={3} notification="!" />
        <Tab label="Settings" value={4} />
      </TabList>
      <TabsBody>
        <TabPanel value={1}>
          <div className="p-16 bg-lightSand">Overview panel</div>
        </TabPanel>
        <TabPanel value={2}>
          <div className="p-16 bg-saffron">3 unread messages</div>
        </TabPanel>
        <TabPanel value={3}>
          <div className="p-16 bg-lavender">Alerts panel</div>
        </TabPanel>
        <TabPanel value={4}>
          <div className="p-16 bg-lightSand">Settings panel</div>
        </TabPanel>
      </TabsBody>
    </Tabs>
  );
}
```

```tsx
'use client';

import { Tab, TabList, TabPanel, Tabs, TabsBody } from '../ui/tabs/Tabs';

export function TabsWithinTabsDemo() {
  const handleSelect = (context: { value: number }) => {
    console.log('Selected tab:', context.value);
  };

  return (
    <Tabs compacted selected={1}>
      <TabList>
        <Tab label="Au ski" value={1} onSelect={handleSelect} />
        <Tab label="Au soleil" value={2} onSelect={handleSelect} />
        <Tab label="En croisière" value={3} onSelect={handleSelect} />
      </TabList>
      <TabsBody>
        <TabPanel value={1}>
          <div className="bg-lightSand p-16">
            <h4>Au ski</h4>
            <Tabs selected={2} compacted>
              <TabList constrained>
                <Tab label="Sur les pistes" value={1} onSelect={handleSelect} />
                <Tab label="A la patinoire" value={2} onSelect={handleSelect} />
              </TabList>
              <TabsBody>
                <TabPanel value={1}>
                  <div>Sur les pistes</div>
                </TabPanel>
                <TabPanel value={2}>
                  <div>A la patinoire</div>
                </TabPanel>
              </TabsBody>
            </Tabs>
          </div>
        </TabPanel>
        <TabPanel value={2}>
          <div className="bg-saffron p-16">
            <h4>Au soleil</h4>
            <Tabs selected={2} compacted>
              <TabList constrained>
                <Tab label="A la plage" value={1} onSelect={handleSelect} />
                <Tab label="A la piscine" value={2} onSelect={handleSelect} />
              </TabList>
              <TabsBody>
                <TabPanel value={1}>
                  <div>A la plage</div>
                </TabPanel>
                <TabPanel value={2}>
                  <div>A la piscine</div>
                </TabPanel>
              </TabsBody>
            </Tabs>
          </div>
        </TabPanel>
        <TabPanel value={3}>
          <div className="bg-lavender p-16">
            <h4>En croisière</h4>
            <Tabs selected={2} compacted>
              <TabList constrained>
                <Tab label="Au Karting" value={1} onSelect={handleSelect} />
                <Tab label="Au minigolf" value={2} onSelect={handleSelect} />
              </TabList>
              <TabsBody>
                <TabPanel value={1}>
                  <div>Au Karting</div>
                </TabPanel>
                <TabPanel value={2}>
                  <div>Au minigolf</div>
                </TabPanel>
              </TabsBody>
            </Tabs>
          </div>
        </TabPanel>
      </TabsBody>
    </Tabs>
  );
}
```

```tsx
import { Tab, TabList, TabPanel, Tabs, TabsBody, TabSelect } from '../ui/tabs/Tabs';

export function TabsWithSelectDemo() {
  const tabs = new Array(3).fill(undefined).map((_, n) => ({ label: `${n + 1}`, value: n + 1 }));

  const bgs = (count: number) => {
    switch (count % 3) {
      case 0:
        return 'bg-lightSand';
      case 1:
        return 'bg-saffron';
      case 2:
        return 'bg-lavender';
      default:
        return 'bg-sienna';
    }
  };
  return (
    <Tabs className="w-full" selected={1}>
      <TabSelect aria-label="Choose a tab" className="md:hidden w-full" items={tabs} />
      <TabList className="hidden md:flex">
        {tabs.map((tab) => {
          return <Tab key={tab.label} label={`Tab - ${tab.label}`} value={tab.value} />;
        })}
      </TabList>
      <TabsBody>
        {tabs.map((tab) => {
          return (
            <TabPanel key={tab.label} value={tab.value}>
              <div className={`p-16 ${bgs(tab.value)}`}>Panel - {tab.label}</div>
            </TabPanel>
          );
        })}
      </TabsBody>
    </Tabs>
  );
}
```

## Notes
