A set of tab panels that are displayed one at a time.

Usage

Items

Use the items prop as an array of objects with the following properties:

  • label?: string
  • icon?: string
  • avatar?: AvatarProps
  • content?: string
  • value?: string | number
  • disabled?: boolean
  • slot?: string
This is the account content.
<script setup lang="ts">
const items = ref([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.'
  }
])
</script>

<template>
  <UTabs :items="items" class="w-full" />
</template>

Content

Use the content prop to control how the Tabs are rendered.

You can set it to false to prevent the Tabs from rendering any content and act as a toggle.

<script setup lang="ts">
const items = ref([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.'
  }
])
</script>

<template>
  <UTabs :content="false" :items="items" class="w-full" />
</template>

You can also choose to only render the content of the active tab by setting content.forceMount to false.

This is the account content.
<script setup lang="ts">
const items = ref([
  {
    label: 'Account',
    icon: 'i-lucide-user',
    content: 'This is the account content.'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock',
    content: 'This is the password content.'
  }
])
</script>

<template>
  <UTabs
    :content="{
      forceMount: false
    }"
    :items="items"
    class="w-full"
  />
</template>
You can inspect the DOM to see that the content of the inactive tab is not rendered.

Color

Use the color prop to change the color of the Tabs.

<script setup lang="ts">
const items = ref([
  {
    label: 'Account'
  },
  {
    label: 'Password'
  }
])
</script>

<template>
  <UTabs color="neutral" :content="false" :items="items" class="w-full" />
</template>

Variant

Use the variant prop to change the variant of the Tabs.

<script setup lang="ts">
const items = ref([
  {
    label: 'Account'
  },
  {
    label: 'Password'
  }
])
</script>

<template>
  <UTabs color="neutral" variant="link" :content="false" :items="items" class="w-full" />
</template>

Size

Use the size prop to change the size of the Tabs.

<script setup lang="ts">
const items = ref([
  {
    label: 'Account'
  },
  {
    label: 'Password'
  }
])
</script>

<template>
  <UTabs :content="false" :items="items" class="w-full" />
</template>

Orientation

Use the orientation prop to change the orientation of the Tabs. Defaults to horizontal.

<script setup lang="ts">
const items = ref([
  {
    label: 'Account'
  },
  {
    label: 'Password'
  }
])
</script>

<template>
  <UTabs orientation="vertical" :content="false" :items="items" class="w-full" />
</template>

Examples

Control active item

You can control the active item by using the default-value prop or the v-model directive with the index of the item.

<script setup lang="ts">
const items = [
  {
    label: 'Account'
  },
  {
    label: 'Password'
  }
]

const active = ref('0')

// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
  setInterval(() => {
    active.value = String((Number(active.value) + 1) % items.length)
  }, 2000)
})
</script>

<template>
  <UTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>
You can also pass the value of one of the items if provided.

With content slot

Use the #content slot to customize the content of each item.

This is the Account tab.

<script setup lang="ts">
const items = [
  {
    label: 'Account',
    icon: 'i-lucide-user'
  },
  {
    label: 'Password',
    icon: 'i-lucide-lock'
  }
]
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #content="{ item }">
      <p>This is the {{ item.label }} tab.</p>
    </template>
  </UTabs>
</template>

With custom slot

Use the slot property to customize a specific item.

Make changes to your account here. Click save when you're done.

<script setup lang="ts">
const items = [
  {
    label: 'Account',
    description: 'Make changes to your account here. Click save when you\'re done.',
    icon: 'i-lucide-user',
    slot: 'account'
  },
  {
    label: 'Password',
    description: 'Change your password here. After saving, you\'ll be logged out.',
    icon: 'i-lucide-lock',
    slot: 'password'
  }
]

const state = reactive({
  name: 'Benjamin Canac',
  username: 'benjamincanac',
  currentPassword: '',
  newPassword: '',
  confirmPassword: ''
})
</script>

<template>
  <UTabs :items="items" variant="link" class="gap-4 w-full" :ui="{ trigger: 'flex-1' }">
    <template #account="{ item }">
      <p class="text-[var(--ui-text-muted)] mb-4">
        {{ item.description }}
      </p>

      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Name" name="name">
          <UInput v-model="state.name" class="w-full" />
        </UFormField>
        <UFormField label="Username" name="username">
          <UInput v-model="state.username" class="w-full" />
        </UFormField>

        <UButton label="Save changes" type="submit" variant="soft" class="self-end" />
      </UForm>
    </template>

    <template #password="{ item }">
      <p class="text-[var(--ui-text-muted)] mb-4">
        {{ item.description }}
      </p>

      <UForm :state="state" class="flex flex-col gap-4">
        <UFormField label="Current Password" name="current" required>
          <UInput v-model="state.currentPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="New Password" name="new" required>
          <UInput v-model="state.newPassword" type="password" required class="w-full" />
        </UFormField>
        <UFormField label="Confirm Password" name="confirm" required>
          <UInput v-model="state.confirmPassword" type="password" required class="w-full" />
        </UFormField>

        <UButton label="Change password" type="submit" variant="soft" class="self-end" />
      </UForm>
    </template>
  </UTabs>
</template>

API

Props

Prop Default Type
as

'div'

any

The element or component this component should render as.

items

TabsItem[]

color

primary

"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"

variant

pill

"link" | "pill"

size

md

"md" | "xs" | "sm" | "lg" | "xl"

orientation

"horizontal"

"vertical" | "horizontal"

The orientation of the tabs.

content

true

boolean | Omit<TabsContentProps, "as" | "asChild" | "value">

The content of the tabs, can be disabled to prevent rendering the content.

labelKey

"label"

string

The key used to get the label from the item.

defaultValue

"0"

string | number

The value of the tab that should be active when initially rendered. Use when you do not need to control the state of the tabs

modelValue

string | number

The controlled value of the tab to activate. Can be bind as v-model.

activationMode

automatic

"automatic" | "manual"

Whether a tab is activated automatically (on focus) or manually (on click).

ui

PartialString<{ root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }>

Slots

Slot Type
leading

{ item: TabsItem; index: number; }

default

{ item: TabsItem; index: number; }

trailing

{ item: TabsItem; index: number; }

content

{ item: TabsItem; index: number; }

Emits

Event Type
update:modelValue

[payload: string | number]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    tabs: {
      slots: {
        root: 'flex items-center gap-2',
        list: 'relative flex p-1 group',
        indicator: 'absolute transition-[translate,width] duration-200',
        trigger: [
          'relative inline-flex items-center shrink-0 data-[state=inactive]:text-[var(--ui-text-muted)] hover:data-[state=inactive]:text-[var(--ui-text)] font-medium rounded-[calc(var(--ui-radius)*1.5)] disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none',
          'transition-colors'
        ],
        content: 'focus:outline-none w-full',
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        label: 'truncate'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          pill: {
            list: 'bg-[var(--ui-bg-elevated)] rounded-[calc(var(--ui-radius)*2)]',
            trigger: 'flex-1 w-full',
            indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-xs'
          },
          link: {
            list: 'border-[var(--ui-border)]',
            indicator: 'rounded-full'
          }
        },
        orientation: {
          horizontal: {
            root: 'flex-col',
            list: 'w-full',
            indicator: 'left-0 w-[var(--radix-tabs-indicator-size)] translate-x-[var(--radix-tabs-indicator-position)]',
            trigger: 'justify-center'
          },
          vertical: {
            list: 'flex-col',
            indicator: 'top-0 h-[var(--radix-tabs-indicator-size)] translate-y-[var(--radix-tabs-indicator-position)]'
          }
        },
        size: {
          xs: {
            trigger: 'px-2 py-1 text-xs gap-1',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs'
          },
          sm: {
            trigger: 'px-2.5 py-1.5 text-xs gap-1.5',
            leadingIcon: 'size-4',
            leadingAvatarSize: '3xs'
          },
          md: {
            trigger: 'px-3 py-1.5 text-sm gap-1.5',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs'
          },
          lg: {
            trigger: 'px-3 py-2 text-sm gap-2',
            leadingIcon: 'size-5',
            leadingAvatarSize: '2xs'
          },
          xl: {
            trigger: 'px-3 py-2 text-base gap-2',
            leadingIcon: 'size-6',
            leadingAvatarSize: 'xs'
          }
        }
      },
      compoundVariants: [
        {
          orientation: 'horizontal',
          variant: 'pill',
          class: {
            indicator: 'inset-y-1'
          }
        },
        {
          orientation: 'horizontal',
          variant: 'link',
          class: {
            list: 'border-b -mb-px',
            indicator: '-bottom-px h-px'
          }
        },
        {
          orientation: 'vertical',
          variant: 'pill',
          class: {
            indicator: 'inset-x-1',
            list: 'items-center'
          }
        },
        {
          orientation: 'vertical',
          variant: 'link',
          class: {
            list: 'border-s -ms-px',
            indicator: '-start-px w-px'
          }
        },
        {
          color: 'primary',
          variant: 'pill',
          class: {
            indicator: 'bg-[var(--ui-primary)]',
            trigger: 'data-[state=active]:text-[var(--ui-bg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          class: {
            indicator: 'bg-[var(--ui-bg-inverted)]',
            trigger: 'data-[state=active]:text-[var(--ui-bg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-border-inverted)]'
          }
        },
        {
          color: 'primary',
          variant: 'link',
          class: {
            indicator: 'bg-[var(--ui-primary)]',
            trigger: 'data-[state=active]:text-[var(--ui-primary)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          class: {
            indicator: 'bg-[var(--ui-bg-inverted)]',
            trigger: 'data-[state=active]:text-[var(--ui-text-highlighted)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-border-inverted)]'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'pill',
        size: 'md'
      }
    }
  }
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.