# Tabs

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/tabs
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=352-2

```tsx
import { type TabItem, Tabs } from '@commenda/pathfinder/tabs';

const entityTabs: TabItem[] = [
  {
    value: 'details',
    label: 'Details',
    content: 'Your workspace name, logo and default entity.',
  },
  {
    value: 'filings',
    label: 'Filings',
    count: 12,
    content: 'Review upcoming and completed filings.',
  },
  {
    value: 'documents',
    label: 'Documents',
    count: 3,
    content: 'Manage the documents for this entity.',
  },
  {
    value: 'activity',
    label: 'Activity',
    content: 'Recent changes to this entity.',
  },
];

export const TabsDefault = () => (
  <Tabs label="Entity views" items={entityTabs} />
);
```


## Installation [#installation]

```sh
pnpm --filter your-app add "@commenda/pathfinder@workspace:*"
```

Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/tabs`. Interactive callbacks belong in a client component in Next.js App Router.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `label` | `string` | required | Required; tablist accessible name |
| `items` | `readonly TabItem[]` | required | Required; value, label, content; optional count and disableConfig |
| `value` | `string \| null` | conditional | Controlled / initial selection; first enabled item if omitted |
| `defaultValue` | `string \| null` | optional | Controlled / initial selection; first enabled item if omitted |
| `onValueChange` | `((value: string \| null) => void)` | conditional | Required with value |
| `orientation` | `"vertical" \| "horizontal"` | optional | horizontal |
| `activationMode` | `"manual" \| "automatic"` | optional | automatic; use manual for slow remote content |
| `divider` | `boolean` | optional | true; horizontal only |
| `panelConfig` | `{ keepMounted?: boolean; className?: string; }` | optional | keepMounted false |
| `overflowConfig` | `{ label?: string; }` | optional | More |
| `id` | `string` | optional | Stable HTML identifier for the component. |
| `className` | `string` | optional | Additional classes for the component root. |

### panelConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | No |  |
| `className` | `string` | No |  |

### overflowConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | No |  |

Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Controlled value requires onValueChange and cannot be combined with defaultValue. Item IDs/values must be unique and stable.

## Usage rules [#usage-rules]

`disableConfig: { isDisabled: boolean; reason?: string }` keeps disabled state and its explanation together. Counts are plain numbers, including zero; do not use a Badge or icons inside the tab label.

Horizontal overflow is measured against the container and moves hidden options into More. The active tab stays visible; choosing an overflow item returns focus to its promoted tab. Before measurement, a local scrolling row keeps options reachable. Extremely long active labels remain horizontally scrollable without overflowing the page.

Arrow keys navigate visible tabs; Home/End reach their ends. Automatic mode also selects on arrow focus. Manual mode activates with Enter/Space. More is a separate menu button, reached with Tab. Hidden options remain available in that menu. Disabled options cannot activate. `keepMounted` retains inactive content without leaving it focusable.

Uncontrolled tabs fall back to the first enabled item after removal/disablement. Controlled callers reconcile changed items and own URL synchronization. `null` means no active panel. These tabs model related panels, not an unrelated route-only sidebar.

## Examples [#examples]

### Vertical [#vertical]


```tsx
import { type TabItem, Tabs } from '@commenda/pathfinder/tabs';

const entityTabs: TabItem[] = [
  {
    value: 'details',
    label: 'Details',
    content: 'Your workspace name, logo and default entity.',
  },
  {
    value: 'filings',
    label: 'Filings',
    count: 12,
    content: 'Review upcoming and completed filings.',
  },
  {
    value: 'documents',
    label: 'Documents',
    count: 3,
    content: 'Manage the documents for this entity.',
  },
  {
    value: 'activity',
    label: 'Activity',
    content: 'Recent changes to this entity.',
  },
];

export const TabsVertical = () => (
  <Tabs label="Settings" orientation="vertical" items={entityTabs} />
);
```


### Overflow [#overflow]


```tsx
import { type TabItem, Tabs } from '@commenda/pathfinder/tabs';

const entityTabs: TabItem[] = [
  {
    value: 'details',
    label: 'Details',
    content: 'Your workspace name, logo and default entity.',
  },
  {
    value: 'filings',
    label: 'Filings',
    count: 12,
    content: 'Review upcoming and completed filings.',
  },
  {
    value: 'documents',
    label: 'Documents',
    count: 3,
    content: 'Manage the documents for this entity.',
  },
  {
    value: 'activity',
    label: 'Activity',
    content: 'Recent changes to this entity.',
  },
];

export const TabsOverflow = () => (
  <div style={{ maxWidth: 280, width: '100%' }}>
    <Tabs
      label="Entity views"
      items={[
        ...entityTabs,
        { value: 'billing', label: 'Billing', content: 'Billing details' },
        { value: 'audit', label: 'Audit log', content: 'Audit history' },
      ]}
    />
  </div>
);
```


### Controlled [#controlled]


```tsx
import { useState } from 'react';

import { type TabItem, Tabs } from '@commenda/pathfinder/tabs';

const entityTabs: TabItem[] = [
  {
    value: 'details',
    label: 'Details',
    content: 'Your workspace name, logo and default entity.',
  },
  {
    value: 'filings',
    label: 'Filings',
    count: 12,
    content: 'Review upcoming and completed filings.',
  },
  {
    value: 'documents',
    label: 'Documents',
    count: 3,
    content: 'Manage the documents for this entity.',
  },
  {
    value: 'activity',
    label: 'Activity',
    content: 'Recent changes to this entity.',
  },
];

export const TabsControlled = () => {
  const [value, setValue] = useState<string | null>('details');

  return (
    <div>
      <Tabs
        label="Entity views"
        value={value}
        onValueChange={setValue}
        activationMode="manual"
        panelConfig={{ keepMounted: true }}
        items={[
          ...entityTabs,
          {
            value: 'billing',
            label: 'Billing',
            content: 'Billing',
            disableConfig: {
              isDisabled: true,
              reason: 'Administrator access required',
            },
          },
        ]}
      />
      <p>Selected view: {value}</p>
    </div>
  );
};
```


## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Tabs [#tabs]

Source: [352:10826](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=352-10826), horizontal 352:95, vertical 352:121, docs 354:278.

- `TABS-01` — Source: horizontal height 28, gap 12, Label/12 Strong, 0.5 divider, 2px blue-700 indicator. Counts are plain Label/12 numbers, gap 4; never Badge or icons. Vertical width 180, padding 6/8, radius 6, gap 2, Label/13 Strong, selected pearl-200.
- `TABS-02` — Source/Pathfinder: related panels for one subject. First enabled item is the uncontrolled default. Caller controls URL synchronization; unrelated routes use navigation. Indicator 200ms ease-out, no panel animation, reduced motion immediate.
- `TABS-03` — Source/Pathfinder: More exposes options that do not fit. Measurement reserves More and the active tab; selected hidden tabs become visible and receive focus. Inert copies measure widths; pre-measurement and exceptionally narrow layouts retain local horizontal scrolling. Never discard a tab to solve overflow.
- `TABS-04` — Pathfinder: automatic or manual keyboard activation; disabled items cannot activate. Controlled callers reconcile missing selections; uncontrolled removal/disablement selects the first enabled item. null permits no active panel. keepMounted retains inactive content without focusability; zero counts remain visible.

### Source discrepancies and implementation decisions [#source-discrepancies-and-implementation-decisions]
