Nuxt UI v3-alpha has been released!

Try it out
Components

Tabs

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

Usage

Pass an array to the items prop of the Tabs component. Each item can have the following properties:

  • label - The label of the item.
  • icon - The icon of the item.
  • slot - A key to customize the item with a slot.
  • content - The content to display in the panel by default.
  • disabled - Determines whether the item is disabled or not.
This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Tab1',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Tab2',
  icon: 'i-heroicons-arrow-down-tray',
  disabled: true,
  content: 'And, this is the content for Tab2'
}, {
  label: 'Tab3',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]
</script>

<template>
  <UTabs :items="items" />
</template>

Vertical

You can change the orientation of the tabs by setting the orientation prop to vertical.

This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Tab1',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Tab2',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Tab3',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]
</script>

<template>
  <UTabs :items="items" orientation="vertical" :ui="{ wrapper: 'flex items-center gap-4', list: { width: 'w-48' } }" />
</template>

Default index

You can set the default index of the tabs by setting the default-index prop.

Finally, this is the content for Tab3
<script setup lang="ts">
const items = [{
  label: 'Tab1',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Tab2',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Tab3',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]
</script>

<template>
  <UTabs :items="items" :default-index="2" />
</template>
This will have no effect if you are using a v-model to control the selected index.

Listen to changes

You can listen to changes by using the @change event. The event will emit the index of the selected item.

This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Tab1',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Tab2',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Tab3',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]

function onChange(index) {
  const item = items[index]

  alert(`${item.label} was clicked!`)
}
</script>

<template>
  <UTabs :items="items" @change="onChange" />
</template>

You can use the content prop and set it to false to avoid the rendering of the HTML content if you don't need it.

Control the selected index

Use a v-model to control the selected index.

This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Tab1',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Tab2',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Tab3',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]

const route = useRoute()
const router = useRouter()

const selected = computed({
  get() {
    const index = items.findIndex(item => item.label === route.query.tab)
    if (index === -1) {
      return 0
    }

    return index
  },
  set(value) {
    // Hash is specified here to prevent the page from scrolling to the top
    router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
  }
})
</script>

<template>
  <UTabs v-model="selected" :items="items" />
</template>
In this example, we are binding tabs to the route query. Refresh the page to see the selected tab change.

Slots

You can use slots to customize the buttons and items content of the Accordion.

default

Use the #default slot to customize the content of the trigger buttons. You will have access to the item, index, selected and disabled in the slot scope.

This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Getting Started',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Installation',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Theming',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #default="{ item, index, selected }">
      <span class="truncate" :class="[selected && 'text-primary-500 dark:text-primary-400']">{{ index + 1 }}. {{ item.label }}</span>
    </template>
  </UTabs>
</template>

icon New

Use the #icon slot to customize the icon of the trigger buttons. You will have access to the item, index, selected and disabled in the slot scope.

This is the content shown for Tab1
<script setup lang="ts">
const items = [{
  label: 'Getting Started',
  icon: 'i-heroicons-information-circle',
  content: 'This is the content shown for Tab1'
}, {
  label: 'Installation',
  icon: 'i-heroicons-arrow-down-tray',
  content: 'And, this is the content for Tab2'
}, {
  label: 'Theming',
  icon: 'i-heroicons-eye-dropper',
  content: 'Finally, this is the content for Tab3'
}]
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #icon="{ item, selected }">
      <UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0 me-2" :class="[selected && 'text-primary-500 dark:text-primary-400']" />
    </template>
  </UTabs>
</template>

item

Use the #item slot to customize the items content. You will have access to the item, index and selected properties in the slot scope.

Account

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

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

const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const passwordForm = reactive({ currentPassword: '', newPassword: '' })

function onSubmit(form) {
  console.log('Submitted form:', form)
}
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #item="{ item }">
      <UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : passwordForm)">
        <template #header>
          <p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
            {{ item.label }}
          </p>
          <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
            {{ item.description }}
          </p>
        </template>

        <div v-if="item.key === 'account'" class="space-y-3">
          <UFormGroup label="Name" name="name">
            <UInput v-model="accountForm.name" />
          </UFormGroup>
          <UFormGroup label="Username" name="username">
            <UInput v-model="accountForm.username" />
          </UFormGroup>
        </div>
        <div v-else-if="item.key === 'password'" class="space-y-3">
          <UFormGroup label="Current Password" name="current" required>
            <UInput v-model="passwordForm.currentPassword" type="password" required />
          </UFormGroup>
          <UFormGroup label="New Password" name="new" required>
            <UInput v-model="passwordForm.newPassword" type="password" required />
          </UFormGroup>
        </div>

        <template #footer>
          <UButton type="submit" color="black">
            Save {{ item.key === 'account' ? 'account' : 'password' }}
          </UButton>
        </template>
      </UCard>
    </template>
  </UTabs>
</template>

You can also pass a slot property to customize a specific item.

Account

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

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

const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
const passwordForm = reactive({ currentPassword: '', newPassword: '' })

function onSubmitAccount() {
  console.log('Submitted form:', accountForm)
}

function onSubmitPassword() {
  console.log('Submitted form:', passwordForm)
}
</script>

<template>
  <UTabs :items="items" class="w-full">
    <template #account="{ item }">
      <UCard @submit.prevent="onSubmitAccount">
        <template #header>
          <p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
            {{ item.label }}
          </p>
          <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
            Make changes to your account here. Click save when you're done.
          </p>
        </template>

        <UFormGroup label="Name" name="name" class="mb-3">
          <UInput v-model="accountForm.name" />
        </UFormGroup>
        <UFormGroup label="Username" name="username">
          <UInput v-model="accountForm.username" />
        </UFormGroup>

        <template #footer>
          <UButton type="submit" color="black">
            Save account
          </UButton>
        </template>
      </UCard>
    </template>

    <template #password="{ item }">
      <UCard @submit.prevent="onSubmitPassword">
        <template #header>
          <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
            {{ item.label }}
          </h3>
          <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
            Change your password here. After saving, you'll be logged out.
          </p>
        </template>

        <UFormGroup label="Current Password" name="current" required class="mb-3">
          <UInput v-model="passwordForm.currentPassword" type="password" required />
        </UFormGroup>
        <UFormGroup label="New Password" name="new" required>
          <UInput v-model="passwordForm.newPassword" type="password" required />
        </UFormGroup>

        <template #footer>
          <UButton type="submit" color="black">
            Save password
          </UButton>
        </template>
      </UCard>
    </template>
  </UTabs>
</template>

Props

ui
{ wrapper?: string; container?: string; base?: string; list?: DeepPartial<{ base: string; background: string; rounded: string; shadow: string; padding: string; height: string; width: string; marker: { wrapper: string; base: string; background: string; rounded: string; shadow: string; }; tab: { ...; }; }, any>; } & {...
{}
orientation
"horizontal" | "vertical"
"horizontal"
modelValue
number
undefined
defaultIndex
number
0
items
TabItem[]
[]
unmount
boolean
false
content
boolean
true

Config

{
  wrapper: 'relative space-y-2',
  container: 'relative w-full',
  base: 'focus:outline-none',
  list: {
    base: 'relative',
    background: 'bg-gray-100 dark:bg-gray-800',
    rounded: 'rounded-lg',
    shadow: '',
    padding: 'p-1',
    height: 'h-10',
    width: 'w-full',
    marker: {
      wrapper: 'absolute top-[4px] left-[4px] duration-200 ease-out focus:outline-none',
      base: 'w-full h-full',
      background: 'bg-white dark:bg-gray-900',
      rounded: 'rounded-md',
      shadow: 'shadow-sm'
    },
    tab: {
      base: 'relative inline-flex items-center justify-center flex-shrink-0 w-full ui-focus-visible:outline-0 ui-focus-visible:ring-2 ui-focus-visible:ring-primary-500 dark:ui-focus-visible:ring-primary-400 ui-not-focus-visible:outline-none focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors duration-200 ease-out',
      background: '',
      active: 'text-gray-900 dark:text-white',
      inactive: 'text-gray-500 dark:text-gray-400',
      height: 'h-8',
      padding: 'px-3',
      size: 'text-sm',
      font: 'font-medium',
      rounded: 'rounded-md',
      shadow: '',
      icon: 'w-4 h-4 flex-shrink-0 me-2'
    }
  }
}