Universal Component

Tabs

Windows Tabs

This page was migrated by AI, please review carefully

Migration is complete, but please validate against source code and manual review.

Tabs

Windows Tabs

Basic Usage

Tabs

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <div style="height: 320px;">
    <TxTabs v-model="active">
      <TxTabItem name="General" icon-class="i-carbon-settings" activation>
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            General
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            Basic settings content
          </p>
        </div>
      </TxTabItem>
      <TxTabItem name="Account" icon-class="i-carbon-user">
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            Account
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            Account settings content
          </p>
        </div>
      </TxTabItem>
      <TxTabItem name="About" icon-class="i-carbon-information">
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            About
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            About content
          </p>
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

Indicator Showcase

Indicator variants & motions

Demo will load when visible.
<script setup lang="ts">
import { computed, ref } from 'vue'

type Motion = 'stretch' | 'warp' | 'glide' | 'snap' | 'spring'

type TabValue = 'A' | 'B' | 'C'

const motion = ref<Motion>('stretch')
const active = ref<TabValue>('A')

const variants = computed(() => {
  return [
    { value: 'line', label: 'line' },
    { value: 'pill', label: 'pill' },
    { value: 'block', label: 'block' },
    { value: 'dot', label: 'dot' },
    { value: 'outline', label: 'outline' },
  ] as const
})

const motionOptions = [
  { value: 'stretch', label: 'stretch' },
  { value: 'warp', label: 'warp' },
  { value: 'glide', label: 'glide' },
  { value: 'snap', label: 'snap' },
  { value: 'spring', label: 'spring' },
] as const

function next() {
  active.value = active.value === 'A' ? 'B' : active.value === 'B' ? 'C' : 'A'
}
</script>

<template>
  <div class="tx-demo tx-demo__col" style="max-width: 860px;">
    <TxCard variant="plain" background="mask" :padding="14" :radius="14">
      <div class="tx-demo__row" style="gap: 10px; flex-wrap: wrap;">
        <label class="tx-demo__row" style="gap: 8px;">
          <span class="tx-demo__label">motion</span>
          <TuffSelect v-model="motion" style="min-width: 190px;">
            <TuffSelectItem v-for="opt in motionOptions" :key="opt.value" :value="opt.value" :label="opt.label" />
          </TuffSelect>
        </label>

        <label class="tx-demo__row" style="gap: 8px;">
          <span class="tx-demo__label">auto</span>
          <TxButton size="small" @click="next">Next</TxButton>
        </label>

        <div style="opacity: 0.7; font-size: 12px;">
          active: <b>{{ active }}</b>
        </div>
      </div>
    </TxCard>

    <div class="tx-demo__col" style="gap: 12px;">
      <TxCard
        v-for="v in variants"
        :key="v.value"
        variant="plain"
        background="mask"
        :padding="12"
        :radius="14"
      >
        <div class="tx-demo__label" style="margin-bottom: 8px;">
          {{ v.label }}
        </div>

        <TxTabs
          v-model="active"
          placement="top"
          :content-scrollable="false"
          :indicator-variant="v.value"
          :indicator-motion="motion"
          :animation="{ indicator: { durationMs: 180 }, content: true }"
        >
          <TxTabItem name="A" activation>
            Overview
          </TxTabItem>
          <TxTabItem name="B">
            Features
          </TxTabItem>
          <TxTabItem name="C">
            Pricing
          </TxTabItem>
        </TxTabs>
      </TxCard>
    </div>
  </div>
</template>

Dynamic Content Size (manual, rich content)

Dynamic Content (manual)

Demo will load when visible.
<script setup lang="ts">
import { computed, ref } from 'vue'

const active = ref<'Overview' | 'Details' | 'Form'>('Overview')

const expanded = ref(false)
const count = ref(3)
const items = computed(() => Array.from({ length: count.value }).map((_, i) => `Item ${i + 1}`))

const query = ref('')
</script>

<template>
  <div style="display: grid; gap: 10px; min-height: 120px; max-width: 100%;">
    <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
      <TxButton size="small" @click="expanded = !expanded">
        Toggle details
      </TxButton>
      <TxButton size="small" :disabled="count <= 0" @click="count--">
        - Item
      </TxButton>
      <TxButton size="small" @click="count++">
        + Item
      </TxButton>
    </div>

    <TxTabs
      v-model="active"
      placement="left"
      :content-scrollable="false"
      auto-width
      :animation="{ size: { enabled: true, durationMs: 260, easing: 'ease' } }"
    >
      <TxTabItem name="Overview" activation icon-class="i-carbon-dashboard">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="font-weight: 650;">
              Overview
            </div>
            <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
              This tab is intentionally compact.
            </div>
            <div style="display: flex; gap: 8px; align-items: center;">
              <div style="font-size: 12px;">
                Search
              </div>
              <div style="width: 220px; max-width: 100%;">
                <TxSearchInput v-model="query" placeholder="Try typing..." />
              </div>
            </div>
          </div>
        </TxCard>
      </TxTabItem>

      <TxTabItem name="Details" icon-class="i-carbon-list">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
              <div style="font-weight: 650;">
                Details
              </div>
              <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                Items: {{ items.length }}
              </div>
            </div>

            <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
              <div
                v-for="it in items"
                :key="it"
                style="border-radius: 12px; padding: 10px; border: 1px solid color-mix(in srgb, var(--tx-border-color, #dcdfe6) 65%, transparent);"
              >
                <div style="font-weight: 600;">
                  {{ it }}
                </div>
                <div style="margin-top: 4px; font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                  Dynamic grid cell
                </div>
              </div>
            </div>

            <div v-if="expanded" style="display: flex; flex-direction: column; gap: 6px;">
              <div style="font-weight: 600;">
                Expanded block
              </div>
              <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                This area appears/disappears and should trigger AutoSizer refresh.
              </div>
              <div
                style="height: 110px; border-radius: 12px; background: color-mix(in srgb, var(--tx-color-primary, #409eff) 12%, transparent);"
              />
            </div>
          </div>
        </TxCard>
      </TxTabItem>

      <TxTabItem name="Form" icon-class="i-carbon-settings">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="font-weight: 650;">
              Form
            </div>
            <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;">
              <TxSearchInput v-model="query" placeholder="Field A" />
              <TxSearchInput v-model="query" placeholder="Field B" />
            </div>

            <div
              style="height: 160px; border-radius: 12px; background: color-mix(in srgb, var(--tx-color-success, #67c23a) 10%, transparent);"
            />
          </div>
        </TxCard>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

Layout Direction (placement)

Placement + Header Slot

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

const activeTop = ref('A')
const activeRight = ref('General')
const actionWide = ref(false)
</script>

<template>
  <div style="display: flex; flex-direction: column; gap: 12px; align-items: stretch;">
    <div style="height: 240px;">
      <TxTabs v-model="activeTop" placement="top" auto-width :animation="{ indicator: { durationMs: 220, easing: 'ease' } }">
        <TxTabHeader v-slot="{ props }">
          <div style="display: flex; align-items: center; width: 100%; padding: 10px 12px;">
            <div style="font-weight: 600;">
              {{ props.node?.props?.name }}
            </div>
          </div>
        </TxTabHeader>

        <template #nav-right>
          <TxButton size="small" type="primary" @click="actionWide = !actionWide">
            {{ actionWide ? 'More Actions' : 'Action' }}
          </TxButton>
        </template>

        <TxTabItem name="A" activation>
          <div style="padding: 8px;">
            Top - A
          </div>
        </TxTabItem>
        <TxTabItem name="B">
          <div style="padding: 8px;">
            Top - B
          </div>
        </TxTabItem>
        <TxTabItem name="C">
          <div style="padding: 8px;">
            Top - C
          </div>
        </TxTabItem>
      </TxTabs>
    </div>

    <div style="height: 240px;">
      <TxTabs v-model="activeRight" placement="right">
        <TxTabItem name="General" icon-class="i-carbon-settings" activation>
          <div style="padding: 8px;">
            Right - General
          </div>
        </TxTabItem>
        <TxTabItem name="Account" icon-class="i-carbon-user">
          <div style="padding: 8px;">
            Right - Account
          </div>
        </TxTabItem>
      </TxTabs>
    </div>
  </div>
</template>

Height Follows Content (animation.size)

Auto Size (contentScrollable=false)

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <div style="min-height: 120px;">
    <TxTabs
      v-model="active"
      placement="left"
      :content-scrollable="false"
      :animation="{ size: { enabled: true, durationMs: 260, easing: 'ease' } }"
    >
      <TxTabItem name="Long" activation>
        <div style="padding: 10px;">
          <div style="font-weight: 600; margin-bottom: 8px;">
            Long Content
          </div>
          <div style="height: 260px; border-radius: 10px; background: color-mix(in srgb, var(--tx-color-primary, #409eff) 12%, transparent);" />
        </div>
      </TxTabItem>
      <TxTabItem name="Short">
        <div style="padding: 10px;">
          <div style="font-weight: 600; margin-bottom: 8px;">
            Short Content
          </div>
          <div style="height: 90px; border-radius: 10px; background: color-mix(in srgb, var(--tx-color-success, #67c23a) 12%, transparent);" />
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

Closing Animations (indicator/content)

Disable Animations

Demo will load when visible.
<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <div style="height: 240px;">
    <TxTabs
      v-model="active"
      placement="bottom"
      :animation="{ indicator: false, content: false }"
    >
      <TxTabItem name="Left1" activation>
        <div style="padding: 10px;">
          Bottom - No animations
        </div>
      </TxTabItem>
      <TxTabItem name="Left2">
        <div style="padding: 10px;">
          Bottom - No animations 2
        </div>
      </TxTabItem>
      <TxTabItem name="Left3">
        <div style="padding: 10px;">
          Bottom - No animations 3
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

API

TxTabs Props

PropTypeDefaultDescription
modelValuestring-Controlled active tab name.
defaultValuestring-Initial active tab name when modelValue is not provided.
placement'left' | 'right' | 'top' | 'bottom''left'Navigation placement; invalid values fall back to left.
offsetnumber0Extra indicator offset along the active tab axis.
navMinWidthnumber220Minimum navigation width for vertical placements.
navMaxWidthnumber320Maximum navigation width for vertical placements.
contentPaddingnumber12Padding applied to the main content panel.
contentScrollablebooleantrueWrap content in an internal scroll container unless size animation needs direct measurement.
borderlessbooleanfalseRemove the outer border/background for embedded layouts.
autoHeightbooleanfalseAnimate content height changes through AutoSizer.
autoWidthbooleanfalseAnimate content width changes through AutoSizer.
indicatorVariant'line' | 'pill' | 'block' | 'dot' | 'outline''line'Indicator style; invalid values fall back to line.
indicatorMotion'stretch' | 'warp' | 'glide' | 'snap' | 'spring''stretch'Indicator transition motion class; invalid values fall back to stretch.
indicatorMotionStrengthnumber1Non-negative CSS variable controlling indicator motion strength.
animationTabsAnimation-Per-area animation configuration for size, nav, indicator, and content transitions.
animation.sizeboolean | { enabled?; durationMs?; easing? }-Enable and configure AutoSizer size animation; defaults to autoHeightDurationMs / autoHeightEasing.
animation.navboolean | { enabled?; durationMs?; easing? }-Enable and configure nav transition CSS variables.
animation.indicatorboolean | { enabled?; durationMs?; easing? }-Enable and configure indicator transition CSS variables.
animation.contentboolean | { enabled? }-Enable or disable the content zoom class during tab switches.
autoHeightDurationMsnumber250Default size animation duration.
autoHeightEasingstringeaseDefault size animation easing.

Slots

NameParamsDescription
default-Provide TxTabItem, optional TxTabItemGroup, and optional TxTabHeader nodes.
nav-right-Render extra controls at the end of the nav bar.

Expose

NameTypeDescription
refresh() => voidForward to the internal AutoSizer refresh().
flip(action: () => void | Promise<void>) => Promise<void>Run an action inside the internal AutoSizer FLIP transition when available.
action(fn: (el: HTMLElement) => void | Promise<void>, optionsOrDetect?: any) => Promise<any>Forward to the internal AutoSizer action helper.
size() => { width: number; height: number } | undefinedReturn the latest measured AutoSizer size.

TxTabItem Props

PropTypeDefaultDescription
namestring-Unique tab name used as the active value.
iconClassstring''Optional icon class rendered before the tab label.
disabledbooleanfalseBlock click/Enter activation and mark the tab disabled.
activationbooleanfalseMark this tab as the uncontrolled default when no modelValue or defaultValue is provided.

Events

EventParamsDescription
changestringEmitted after user activation or uncontrolled default activation changes the active tab.
update:modelValuestringEmitted with the active tab name for v-model updates.