<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Tabs

Tabs that switch between panels in place: an accessible tab list with roving focus, arrow-key navigation, a line that slides to the selected tab, and 44px tabs on touch screens. Pass the tabs and their panels as `items`. Under forced colours (Windows contrast themes) each tab keeps a border and the selected one is filled with Highlight.

```tsx
import { Tabs } from "@/components/ouro/tabs/tabs";
```

## Example

```tsx
<Tabs
  label="Recipe"
  items={[
    { value: "ingredients", label: "Ingredients", content: <IngredientList /> },
    { value: "method", label: "Method", content: <Steps /> },
  ]}
/>
<Tabs
  label="Recipes"
  currentPath={location.pathname}
  items={[
    { value: "all", label: "All", href: "/recipes" },
    { value: "mine", label: "Mine", href: "/my-recipes" },
  ]}
/>
```

## Usage

When each tab is a view of one page with a URL of its own (a section's pages, a search's scopes), give each item an `href` instead of `content`: the tabs render as a `<nav>` of links in the same look, through your router's link, with the current one marked `aria-current="page"` from `currentPath`, and Tab moves between them as between any links.

Use the parts in `tabs-parts` when `items` cannot express your layout.

## Use instead when

- Use `SegmentedControl` instead when the choice changes how one view looks (list or grid) rather than which content shows.
- Use `Navbar` or `Sidebar` instead for the site's own navigation.

## Props

Type: `TabsProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `activation` | `"automatic" \| "manual"` | `"automatic"` | How the keyboard selects a tab with a panel. `automatic` selects it as soon as the arrow keys focus it, which suits panels that show at once. `manual` moves focus only and waits for Enter or Space, for panels that load slowly. |
| `className` | `string` |  | Layout only: margins, width, grid placement (`mt-6`). Appearance classes warn in development. |
| `currentPath` | `string` |  | Links only: the path of the page being shown, from your router, with its query when the tabs differ by query (`location.pathname + location.search`). The link at that path whose parameters the path carries gets `aria-current="page"` and the current look; with links at other paths, the longest one the path sits under (`/recipes` for `/recipes/12`). |
| `defaultValue` | `string` | the first enabled item | The tab selected first, when the component keeps its own state. Panels only. |
| `items` (required) | `readonly TabsItem[] \| readonly TabsLinkItem[]` |  | The tabs, in order: each with the panel it shows (`content`), or each a link to its own page (`href`). Links render as a `<nav>` of links named by `label`, and Tab moves between them as between any links. |
| `label` (required) | `string` |  | Names the tab list for screen readers ("Recipe sections"), or the nav of links. It is not shown: the tabs' own labels are the visible text. |
| `onValueChange` | `((value: string) => void)` |  | Called with the new tab's value when the person selects a tab. Panels only: a link opens its page. |
| `orientation` | `"vertical" \| "horizontal"` | `"horizontal"` | `horizontal` puts the tabs in a row above the panel; `vertical` in a column beside it, from the `sm` breakpoint (640px) up, and in a row on a phone. With panels, the arrow keys follow: left and right in a row, up and down in a column. |
| `ref` | `Ref<HTMLDivElement>` |  | Reaches the rendered element. |
| `value` | `string` |  | The selected tab's value, when you control it. Pair it with `onValueChange`. With links, the current page's value, when `currentPath` cannot tell; it wins over `currentPath`. |

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `activateOnFocus`: Not a prop. Ouro UI uses `activation`: `<Tabs activation="manual">`.
- `onChange`: Not a prop. Ouro UI uses `onValueChange`, which gets the tab's value: `<Tabs onValueChange={(value) => …}>`.
- `pathname`: Not a prop. Tabs that are links use `currentPath`: `<Tabs currentPath={pathname}>`.
- `tabs`: Not a prop. Ouro UI uses `items`: `<Tabs items={[{ value, label, content }]}>`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`). Your classes always win over Ouro UI's: Tabs merges `className` last with `cn` (tailwind-merge), so a class that conflicts with its own replaces it.[^layout-classes]

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Parts

When these props cannot express your layout, build it from the parts in `@/components/ouro/tabs/tabs-parts`. See [parts.md](parts.md#tabs-parts) for the required nesting.

## Types

### `TabsItem`

One tab and its panel.

```ts
interface TabsItem {
  /** Identifies the tab: `value`, `defaultValue` and `onValueChange` use it. Unique within the tabs. */
  value: string;
  /** The tab's text. Keep it short: one or two words. */
  label: ReactNode;
  /** What the panel shows while this tab is selected. */
  content: ReactNode;
  /** Shows the tab but keeps it from being selected. */
  disabled?: boolean | undefined;
  /** Not set: a tab with a panel. Give every item an `href`, and no `content`, for tabs that are links. */
  href?: never;
}
```

### `TabsLinkItem`

One tab that is a link to a page of its own.

```ts
interface TabsLinkItem {
  /** Identifies the tab: `value` names the current one with it. Unique within the tabs. */
  value: string;
  /** The tab's text. Keep it short: one or two words. */
  label: ReactNode;
  /**
   * The page the tab opens. Internal paths render through `OuroProvider`'s `linkComponent`
   * (your router's link).
   */
  href: string;
  /** Not set: the page at `href` is the tab's content. */
  content?: never;
  /** A link cannot be disabled. Leave the item out instead. */
  disabled?: never;
}
```

[^layout-classes]: test:registry/ouro/components/ouro/tabs/tabs.test.tsx::Tabs > lets app layout classes win verify:testExists(registry/ouro/components/ouro/tabs/tabs.test.tsx::Tabs > lets app layout classes win)
