# Tabs

> Switches between views of the same subject without leaving the page; segmented or underline style with a sliding indicator, built on Base UI Tabs.

Source: https://docs.nasaqui.com/components/tabs

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/tabs.json
```

Shows one panel at a time out of several that describe the same subject: screenshots of a product, the sections of a settings page. It wraps Base UI Tabs, so roles, focus and keyboard behaviour come from Base UI. Nasaq adds two visual variants and a sliding indicator that follows the active tab.

## When to use

- Two to six views of the same thing, where the user compares or switches back and forth.
- Page sections on one URL (`variant="underline"`).
- A few short options inside a card or dialog (`variant="segmented"`).

## When not to use

- Site or app navigation between pages: use links in [`AppShell`](https://docs.nasaqui.com/components/app-shell) navigation, not tabs.
- Content that should all be visible at once or read in order: use plain sections.
- Choosing a value in a form: use a radio group or select.

## Import

```tsx
import { Tabs, TabsList, TabsTab, TabsIndicator, TabsPanel, type TabsListProps } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from "@fadymondy/nasaq/web";

export function ProductTabs() {
  return (
    <Tabs defaultValue="board">
      <TabsList>
        <TabsTab value="board">Board</TabsTab>
        <TabsTab value="timeline">Timeline</TabsTab>
        <TabsIndicator />
      </TabsList>
      <TabsPanel value="board">Board view</TabsPanel>
      <TabsPanel value="timeline">Timeline view</TabsPanel>
    </Tabs>
  );
}
```

## Anatomy

```
Tabs                     data-slot="tabs"            Base UI Tabs.Root (flex column, gap-4)
├─ TabsList              data-slot="tabs-list"       data-variant="segmented|underline"
│  ├─ TabsTab            data-slot="tabs-tab"        one per panel (button, role=tab)
│  └─ TabsIndicator      data-slot="tabs-indicator"  put last inside the list
└─ TabsPanel             data-slot="tabs-panel"      one per tab (role=tabpanel)
```

## API

All parts forward their remaining props to the matching Base UI part (`Tabs.Root`, `Tabs.List`, `Tabs.Tab`, `Tabs.Indicator`, `Tabs.Panel`), including `className`, which is merged with the defaults.

### `Tabs`

`ComponentProps<typeof BaseTabs.Root>`. Common props from Base UI:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value?` | `string \| number` | none | Controlled active tab. |
| `defaultValue?` | `string \| number` | first tab | Initial active tab when uncontrolled. |
| `onValueChange?` | `(value) => void` | none | Called when the active tab changes. |
| `orientation?` | `"horizontal" \| "vertical"` | `"horizontal"` | Sets which arrow keys move between tabs. |
| `className?` | `string` | none | Merged onto the root (`flex flex-col gap-4`). |

### `TabsList`

`TabsListProps extends ComponentProps<typeof BaseTabs.List>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `"segmented" \| "underline"` | `"segmented"` | `segmented`: tabs in a tinted track, for a few short options. `underline`: a line under the active tab, for page sections. Also applied to `TabsTab` and `TabsIndicator`. |
| `activateOnFocus?` | `boolean` | `false` | Base UI: select a tab as soon as it is focused with the arrow keys. |
| `className?` | `string` | none | Merged onto the list. The list scrolls sideways when tabs do not fit. |

### `TabsTab`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| number` | required | Matches the `value` of its `TabsPanel`. |
| `disabled?` | `boolean` | `false` | Not selectable; dimmed. |
| `className?` | `string` | none | Merged onto the tab. |

### `TabsIndicator`

Takes Base UI `Tabs.Indicator` props. Renders the sliding highlight (behind the tab in `segmented`, a 2px line under it in `underline`). Place it last inside `TabsList`.

### `TabsPanel`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| number` | required | Matches the `value` of its `TabsTab`. |
| `keepMounted?` | `boolean` | `false` | Base UI: keep the hidden panel in the DOM. |
| `className?` | `string` | none | Merged onto the panel. |

## Examples

### Underline variant for page sections

```tsx
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from "@fadymondy/nasaq/web";

export function SettingsTabs() {
  return (
    <Tabs defaultValue="general">
      <TabsList variant="underline">
        <TabsTab value="general">General</TabsTab>
        <TabsTab value="members">Members</TabsTab>
        <TabsTab value="billing" disabled>
          Billing
        </TabsTab>
        <TabsIndicator />
      </TabsList>
      <TabsPanel value="general">General settings</TabsPanel>
      <TabsPanel value="members">Members</TabsPanel>
    </Tabs>
  );
}
```

### Controlled, Arabic

```tsx
import { useState } from "react";
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from "@fadymondy/nasaq/web";

export function ArabicTabs() {
  const [value, setValue] = useState<string | number>("board");
  return (
    <Tabs value={value} onValueChange={setValue} dir="rtl">
      <TabsList>
        <TabsTab value="board">اللوحة</TabsTab>
        <TabsTab value="timeline">الجدول الزمني</TabsTab>
        <TabsIndicator />
      </TabsList>
      <TabsPanel value="board">عرض اللوحة</TabsPanel>
      <TabsPanel value="timeline">عرض الجدول الزمني</TabsPanel>
    </Tabs>
  );
}
```

## Accessibility

Base UI provides `role="tablist"`, `role="tab"` with `aria-selected` and `aria-controls`, and `role="tabpanel"` with `aria-labelledby`.

| Key | Action |
| --- | --- |
| `Tab` | Moves focus into the tab list (to the active tab), then into the active panel. |
| `ArrowRight` / `ArrowLeft` | Next / previous tab. They follow the reading direction: in RTL, `ArrowLeft` moves to the next tab. |
| `Home` / `End` | First / last tab. |
| `ArrowDown` / `ArrowUp` | Next / previous tab when `orientation="vertical"`. |
| `Enter` / `Space` | Selects the focused tab (when `activateOnFocus` is off). |

- `TabsPanel` is focusable and shows a focus ring, so keyboard users can reach panel content.
- The caller localises every tab label. Keep them short; the list does not wrap.
- Disabled tabs cannot be selected and are dimmed to 50%.

## RTL & i18n

- Tab order, the indicator and the scroll direction mirror with `dir="rtl"`. The indicator uses Base UI's measured left and width, so it follows the active tab in both directions.
- Arrow keys follow the reading direction, as in the table above.
- Numbers in labels ("Issues 12"): isolate with `Num` from the numeric component.
- No built-in strings.

## Styling & tokens

- Tokens: `bg-secondary` (segmented track), `bg-background` and `shadow-xs` (segmented indicator), `bg-primary` (underline indicator), `border-border`, `text-muted-foreground`, `text-foreground`, `text-label`, `outline-nq-focus`, `rounded-control`.
- State attributes: `data-active` on the active tab, `data-disabled` on disabled tabs, `data-variant` on the list.
- Target with `[data-slot=tabs-tab]`, `[data-slot=tabs-indicator]`. Extend with `className`. Do not recolour with raw hex.
- Icons inside a tab are sized to `size-4` automatically.

## Do / Don't

- **Do** put `TabsIndicator` last inside `TabsList`.
- **Do** match every `TabsTab` `value` to one `TabsPanel` `value`.
- **Do** use `segmented` for a few short options and `underline` for page sections.
- **Don't** use tabs for navigation between routes.
- **Don't** hide required form fields inside inactive tabs.
- **Don't** add so many tabs that the list must scroll.

## Related

- [Button](https://docs.nasaqui.com/components/button) · [Card](https://docs.nasaqui.com/components/card) · [ScreenshotFrame](https://docs.nasaqui.com/components/screenshot-frame)

## Lab

https://docs.nasaqui.com/?path=/docs/components-navigation-tabs--docs

## Code

### React

```tsx
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from "@fadymondy/nasaq/web";

export function ProductTabs() {
  return (
    <Tabs defaultValue="board">
      <TabsList>
        <TabsTab value="board">Board</TabsTab>
        <TabsTab value="timeline">Timeline</TabsTab>
        <TabsIndicator />
      </TabsList>
      <TabsPanel value="board">Board view</TabsPanel>
      <TabsPanel value="timeline">Timeline view</TabsPanel>
    </Tabs>
  );
}
```

### shadcn

```tsx
import { Tabs, TabsIndicator, TabsList, TabsPanel, TabsTab } from "@/components/ui/tabs";

export function ProductTabs() {
  return (
    <Tabs defaultValue="board">
      <TabsList>
        <TabsTab value="board">Board</TabsTab>
        <TabsTab value="timeline">Timeline</TabsTab>
        <TabsIndicator />
      </TabsList>
      <TabsPanel value="board">Board view</TabsPanel>
      <TabsPanel value="timeline">Timeline view</TabsPanel>
    </Tabs>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqTabs, NqTabsIndicator, NqTabsList, NqTabsPanel, NqTabsTab } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqTabs default-value="board">
    <NqTabsList>
      <NqTabsTab value="board">Board</NqTabsTab>
      <NqTabsTab value="timeline">Timeline</NqTabsTab>
      <NqTabsIndicator />
    </NqTabsList>
    <NqTabsPanel value="board">Board view</NqTabsPanel>
    <NqTabsPanel value="timeline">Timeline view</NqTabsPanel>
  </NqTabs>
</template>
```

### Blade

```blade
<x-nq::tabs default-value="board">
    <x-nq::tabs.list>
        <x-nq::tabs.tab value="board">Board</x-nq::tabs.tab>
        <x-nq::tabs.tab value="timeline">Timeline</x-nq::tabs.tab>
        <x-nq::tabs.indicator />
    </x-nq::tabs.list>
    <x-nq::tabs.panel value="board">Board view</x-nq::tabs.panel>
    <x-nq::tabs.panel value="timeline">Timeline view</x-nq::tabs.panel>
</x-nq::tabs>
```

### HTML + Alpine

```html
<div data-slot="tabs" data-orientation="horizontal" x-data="nqTabs('board')" x-modelable="value" x-id="['nq-tabs']" class="flex flex-col gap-4">
    <div data-slot="tabs-list" data-variant="segmented" aria-orientation="horizontal" x-bind="list"
    class="relative z-0 flex max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden w-fit gap-0.5 rounded-control bg-secondary p-0.5">
    <button type="button" data-slot="tabs-tab" x-bind="tab('board')"
     data-active aria-selected="true"         class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground data-active:text-foreground [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 h-7 rounded-[calc(var(--radius-control)-2px)] px-3">Board</button>
        <button type="button" data-slot="tabs-tab" x-bind="tab('timeline')"
            class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground data-active:text-foreground [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 h-7 rounded-[calc(var(--radius-control)-2px)] px-3">Timeline</button>
        <span data-slot="tabs-indicator" aria-hidden="true"
    class="absolute -z-10 transition-[left,width] duration-200 ease-nq left-[var(--active-tab-left)] w-[var(--active-tab-width)] top-[var(--active-tab-top)] h-[var(--active-tab-height)] rounded-[calc(var(--radius-control)-2px)] bg-background shadow-xs"></span>
</div>
    <div data-slot="tabs-panel" x-bind="panel('board')"
        class="outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">Board view</div>
    <div data-slot="tabs-panel" x-bind="panel('timeline')"
     hidden     class="outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus">Timeline view</div>
</div>
```
