---
title: "Tabs"
description: "A view that switches between panels using interactive tabs."
category: Navigation
status: beta
since: 0.3.4
tags: [navigation, tabs, layout]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
:::TuffDemoWrapper{demo="TabsTabsDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const active = ref('General')
  </script>

  <template>
    <TxTabs v-model="active">
      <TxTabItem name="General" icon-class="i-carbon-settings" activation>
        General settings
      </TxTabItem>
      <TxTabItem name="Account" icon-class="i-carbon-user">
        Account settings
      </TxTabItem>
      <TxTabItem name="About" icon-class="i-carbon-information">
        About
      </TxTabItem>
    </TxTabs>
  </template>
---
:::

### Indicator
`indicatorVariant` sets how the indicator is drawn and `indicatorMotion` how it travels. With `showIndicator` off, the active item paints its own fill.
:::TuffDemoWrapper{demo="TabsIndicatorVariantsMotionsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs
      v-model="active"
      placement="top"
      indicator-variant="pill"
      indicator-motion="stretch"
      :animation="{ content: { type: 'zoom', durationRatio: 0.5 } }"
    >
      <TxTabItem name="A" activation>Overview</TxTabItem>
      <TxTabItem name="B">Features</TxTabItem>
      <TxTabItem name="C">Pricing</TxTabItem>
    </TxTabs>
  </template>
---
:::

### Dynamic Content Size
`animation.size` keeps the tabs' size in step as panel content grows or shrinks.
:::TuffDemoWrapper{demo="TabsDynamicContentManualDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs
      v-model="active"
      placement="left"
      :content-scrollable="false"
      auto-width
      :animation="{ size: { enabled: true, durationMs: 260 } }"
    >
      <TxTabItem name="Overview" activation>…</TxTabItem>
      <TxTabItem name="Details">
        <div v-for="item in items" :key="item">{{ item }}</div>
      </TxTabItem>
    </TxTabs>
  </template>
---
:::

### Placement and Header
`placement` takes four directions. `TxTabHeader` renders a sticky header above the panel, and the `nav-right` slot holds actions at the end of the nav bar.
:::TuffDemoWrapper{demo="TabsPlacementHeaderSlotDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs v-model="active" placement="top">
      <TxTabHeader v-slot="{ props }">
        {{ props.node?.props?.name }}
      </TxTabHeader>

      <template #nav-right>
        <TxButton size="sm">Action</TxButton>
      </template>

      <TxTabItem name="A" activation>A</TxTabItem>
      <TxTabItem name="B">B</TxTabItem>
    </TxTabs>
  </template>
---
:::

### Height Follows Content
:::TuffDemoWrapper{demo="TabsAutoSizeContentScrollableFalseDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs
      v-model="active"
      :content-scrollable="false"
      :animation="{ size: { enabled: true, durationMs: 260 } }"
    >
      <TxTabItem name="Long" activation>…</TxTabItem>
      <TxTabItem name="Short">…</TxTabItem>
    </TxTabs>
  </template>
---
:::

### Disabling Animations
:::TuffDemoWrapper{demo="TabsDisableAnimationsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs v-model="active" placement="bottom" :animation="{ indicator: false, content: false }">
      <TxTabItem name="One" activation>…</TxTabItem>
      <TxTabItem name="Two">…</TxTabItem>
    </TxTabs>
  </template>
---
:::

### Dashboard Navigation
Tabs hold the top-level sections; light actions go in `TxDropdownMenu`, short notes in `TxPopover`, and dense settings in `TxDrawer`.
:::TuffDemoWrapper{demo="ComponentsNavigationShellDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTabs
      v-model="active"
      placement="left"
      :nav-min-width="176"
      indicator-variant="pill"
      indicator-motion="glide"
      auto-height
    >
      <TxTabItem name="Overview" icon-class="i-carbon-dashboard" activation>…</TxTabItem>
      <TxTabItem name="Release" icon-class="i-carbon-rocket">…</TxTabItem>
    </TxTabs>

    <TxDrawer v-model:visible="drawerVisible" title="Release policy">…</TxDrawer>
  </template>
---
:::

### Best Practices

- Keep each `TxTabItem` `name` stable, unique, and equal to the values in `modelValue` / `defaultValue`.
- Prefer `placement="left"` for settings and admin pages; top and bottom tabs suit short secondary switches.
- Inactive panels unmount, so keep state that must survive outside them.
- When a panel's height changes after loading, enable direct measurement (`contentScrollable=false` or `autoHeight`) and call `refresh()` once content settles.
- Keep `nav-right` compact; move dense actions into a dropdown menu or drawer.

## API Reference

### TxTabs

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `modelValue` | `string` | - | Name of the active tab (controlled). |
| `defaultValue` | `string` | - | Initial tab when uncontrolled; selects nothing if no tab matches. |
| `placement` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Where the navigation sits. |
| `offset` | `number` | `0` | Shift (px) of the `line` indicator along the nav axis. |
| `navMinWidth` | `number` | `220` | Minimum nav width for vertical placements. |
| `navMaxWidth` | `number` | `320` | Maximum nav width for vertical placements. |
| `contentPadding` | `number` | `12` | Padding of the content panel. |
| `contentScrollable` | `boolean` | `true` | Wraps content in a scroll container; removed automatically when size animation is on. |
| `borderless` | `boolean` | `false` | Removes the outer border and background. |
| `autoHeight` | `boolean` | `false` | Animates height changes. |
| `autoWidth` | `boolean` | `false` | Animates width changes. |
| `showIndicator` | `boolean` | `true` | Shows the indicator; when off, the active item paints its own fill. |
| `indicatorVariant` | `'line' \| 'pill' \| 'block' \| 'dot' \| 'outline'` | `'line'` | How the indicator is drawn: a line, a raised surface, a tint, a dot, or an inset ring. |
| `indicatorMotion` | `'stretch' \| 'warp' \| 'glide' \| 'snap' \| 'spring'` | `'stretch'` | How it travels: `stretch` lengthens slightly, `warp` lengthens further, `glide` slides rigidly, `snap` settles fastest, `spring` passes the target once and returns. |
| `indicatorMotionStrength` | `number` | `1` | How far the indicator lengthens on the way; `0` slides rigidly. |
| `animation` | `TabsAnimation` | - | Configures the `size`, `nav`, `indicator`, and `content` animations. |
| `animation.size` | `boolean \| { enabled?; durationMs?; easing? }` | derived from `autoHeight` / `autoWidth` | Size animation. |
| `animation.nav` | `boolean \| { enabled?; durationMs?; easing? }` | on, `220ms ease` | Nav width transition. |
| `animation.indicator` | `boolean \| { enabled?; durationMs?; easing? }` | on, `350ms` | Indicator travel; `durationMs` time-scales the springs, `false` lands in place. |
| `animation.content` | `boolean \| { enabled?; type?; durationMs?; durationRatio?; easing? }` | on, `zoom`, `180ms ease` | Panel entrance; `type` is `fade`, `slide`, `zoom`, `blur`, `scale`, or `none`. |
| `autoHeightDurationMs` | `number` | `250` | Default size-animation duration. |
| `autoHeightEasing` | `string` | `ease` | Default size-animation easing. |

#### Events

| Event | Params | Description |
|------|------|------|
| `update:modelValue` | `value: string` | Fires when the active tab changes. |
| `change` | `value: string` | Fires after a user activates a tab; not on parent-driven `modelValue` changes. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | - | `TxTabItem`, `TxTabItemGroup`, and `TxTabHeader` nodes; anything else is ignored. |
| `nav-right` | - | Actions at the end of the nav bar. |

#### Exposed Methods

| Name | Type | Description |
|------|------|------|
| `refresh` | `() => void` | Re-measures the size. |
| `flip` | `(action: () => void \| Promise<void>) => Promise<void>` | Runs a change inside a FLIP transition. |
| `action` | `(fn: (el: HTMLElement \| undefined) => void \| Promise<void>, optionsOrDetect?: any) => Promise<{ changedKeys: string[] } \| any>` | Forwards to the internal AutoSizer `action`. |
| `size` | `() => { width: number; height: number } \| undefined` | The last measured size. |

### TxTabItem

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `name` | `string` | *required* | Unique name, also the active value. |
| `iconClass` | `string` | `''` | Icon class shown before the label. |
| `disabled` | `boolean` | `false` | Prevents activation. |
| `activation` | `boolean` | `false` | Default tab when neither `modelValue` nor `defaultValue` selects one. |
| `active` | `boolean` | `false` | Whether it is active; injected by TxTabs, set it only when used standalone. |

#### Events

| Event | Params | Description |
|------|------|------|
| `click` | - | Fires when used standalone and not disabled. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | - | Panel content; not mounted while inactive. |
| `icon` | - | Custom icon, replacing `iconClass`. |
| `name` | - | Custom label; defaults to `name`. |

### TxTabHeader

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `node` | `unknown` | - | The active `TxTabItem` VNode, passed by TxTabs. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | `{ props: { node?: unknown } }` | Sticky header above the panel. |

### TxTabItemGroup

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `name` | `string` | - | Group label in the navigation. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | - | Child `TxTabItem` nodes. |

## Overview

- When controlled, `modelValue` wins; otherwise `defaultValue`, then `activation`.
- Only direct `TxTabItem`, `TxTabItemGroup`, and `TxTabHeader` children count (fragments included); custom wrappers are ignored.
- Inactive panels are not mounted.
- The indicator is the only selection highlight. It glides to the target with each end on its own spring and never scales.
- The indicator lands in place on first measure, on size changes, with `animation.indicator: false`, and under reduced motion.
- Size animation removes the inner scroll container so the panel can be measured directly.

## Technologies

- The indicator runs on the glide material of `useJellyIndicator`; its `requestAnimationFrame` loop runs only while moving and writes styles directly, without re-rendering.
- `TxTabItemGroup` renders no DOM; TxTabs reads its children and builds the group.
- Source: `packages/tuffex/packages/components/src/tabs/`.

<TuffDocSourceLink />

## Customization

| CSS variable | Used for |
|----------------------------|----------|
| `--tx-border-color` | Outer border and nav divider. |
| `--tx-bg-color` | Container background. |
| `--tx-fill-color` / `--tx-fill-color-light` | Active fill (without indicator) / hover fill. |
| `--tx-color-primary` | The `line`, `dot`, `block`, and `outline` indicators and the active icon. |
| `--tx-surface-raised` / `--tx-elevation-1` / `--tx-border-color-lighter` | The `pill` surface, its shadow, and its inset ring. |
| `--tx-text-color-primary` / `--tx-text-color-regular` / `--tx-text-color-secondary` | Active text, resting text, icons and group labels. |
| `--tx-tab-item-ink` / `--tx-tab-item-icon-ink` | Text and icon color of `.tx-tab-item`. |
| `--tx-tabs-indicator-duration` / `--tx-tabs-indicator-easing` / `--tx-tabs-indicator-strength` | Generated from the indicator props, for hosts to read. |
| `--tx-tabs-content-duration` / `--tx-tabs-content-easing` | Generated from the content animation props. |
| `--tx-tabs-nav-duration` / `--tx-tabs-nav-easing` | Generated from the nav animation props. |
