Lzh on GitHub
一组每次只显示一个的标签页面板。

用法

Items

使用 items prop 作为对象数组,对象包含以下属性:

  • label?: string
  • icon?: string
  • avatar?: AvatarProps
  • content?: string
  • value?: string | number
  • disabled?: boolean
  • slot?: string
  • class?: any
  • ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, label?: ClassNameValue, content?: ClassNameValue }
This is the account content.
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    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)

content prop 设置为 false 以将 Tabs 转换为仅切换控制,不显示任何内容。默认为 true

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    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>

卸载 (Unmount)

使用 unmount-on-hide prop 防止 Tabs 折叠时内容被卸载。默认为 true

This is the account content.
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items = ref<TabsItem[]>([
  {
    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 :unmount-on-hide="false" :items="items" class="w-full" />
</template>
你可以检查 DOM 以查看每个项目的内容正在渲染。

颜色 (Color)

使用 color prop 更改 Tabs 的颜色。

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

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

变体 (Variant)

使用 variant prop 更改 Tabs 的变体。

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

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

尺寸 (Size)

使用 size prop 更改 Tabs 的尺寸。

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

<template>
  <UTabs size="md" variant="pill" :content="false" :items="items" class="w-full" />
</template>

方向 (Orientation)

使用 orientation prop 更改 Tabs 的方向。默认为 horizontal

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

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

示例

控制活动项目

你可以通过使用 default-value prop 或 v-model 指令以及项目的索引来控制活动项目。

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

const items: TabsItem[] = [
  {
    label: 'Account',
    value: 'account'
  },
  {
    label: 'Password',
    value: 'password'
  }
]

const active = computed({
  get() {
    return (route.query.tab as string) || 'account'
  },
  set(tab) {
    // Hash is specified here to prevent the page from scrolling to the top
    router.push({
      path: '/components/tabs',
      query: { tab },
      hash: '#control-active-item'
    })
  }
})
</script>

<template>
  <UTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>
tabs 项目的 value 不能为 0

带内容插槽

使用 #content 插槽自定义每个项目的内容。

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const items: TabsItem[] = [
  {
    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>

带自定义插槽

使用 slot 属性自定义特定项目。

你将可以使用以下插槽:

  • #{{ item.slot }}
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

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

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: 'grow' }">
    <template #account="{ item }">
      <p class="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-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'

"horizontal" | "vertical"

The orientation of the tabs.

content

true

boolean

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.

unmountOnHide

true

boolean

When true, the element will be unmounted on closed state.

activationMode

automatic

"automatic" | "manual"

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

ui

{ root?: ClassNameValue; list?: ClassNameValue; indicator?: ClassNameValue; trigger?: ClassNameValue; leadingIcon?: ClassNameValue; ... 5 more ...; content?: ClassNameValue; }

:content="false" 时,UTabs 将只渲染标签页的标题部分,而不会渲染标签页对应的内容区域。

为什么要设置 content="false"
  • 自定义内容渲染:这通常意味着你希望在 UTabs 组件的外部,手动根据 selectedTab 的值来渲染不同的内容区域。例如,你可能使用 <NuxtPage> 或一个大的 <component :is="currentContentComponent" /> 来根据 selectedTab 动态切换页面或组件,而不是让 UTabs 组件自己管理内容的显示。
  • 性能优化/分离关注点:将内容渲染逻辑与标签页导航逻辑分离,有时可以提供更细粒度的控制,或在某些复杂场景下避免组件嵌套带来的性能问题。

Slots

Slot Type
leading

{ item: TabsItem; index: number; }

default

{ item: TabsItem; index: number; }

trailing

{ item: TabsItem; index: number; }

content

{ item: TabsItem; index: number; }

list-leading

{}

list-trailing

{}

Emits

Event Type
update:modelValue

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: [
          'group relative inline-flex items-center min-w-0 data-[state=inactive]:text-muted hover:data-[state=inactive]:not-disabled:text-default font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75',
          'transition-colors'
        ],
        leadingIcon: 'shrink-0',
        leadingAvatar: 'shrink-0',
        leadingAvatarSize: '',
        label: 'truncate',
        trailingBadge: 'shrink-0',
        trailingBadgeSize: 'sm',
        content: 'focus:outline-none w-full'
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          pill: {
            list: 'bg-elevated rounded-lg',
            trigger: 'grow',
            indicator: 'rounded-md shadow-xs'
          },
          link: {
            list: 'border-default',
            indicator: 'rounded-full',
            trigger: 'focus:outline-none'
          }
        },
        orientation: {
          horizontal: {
            root: 'flex-col',
            list: 'w-full',
            indicator: 'left-0 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position)',
            trigger: 'justify-center'
          },
          vertical: {
            list: 'flex-col',
            indicator: 'top-0 h-(--reka-tabs-indicator-size) translate-y-(--reka-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-primary',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
          }
        },
        {
          color: 'neutral',
          variant: 'pill',
          class: {
            indicator: 'bg-inverted',
            trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
          }
        },
        {
          color: 'primary',
          variant: 'link',
          class: {
            indicator: 'bg-primary',
            trigger: 'data-[state=active]:text-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
          }
        },
        {
          color: 'neutral',
          variant: 'link',
          class: {
            indicator: 'bg-inverted',
            trigger: 'data-[state=active]:text-highlighted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'pill',
        size: 'md'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        tabs: {
          slots: {
            root: 'flex items-center gap-2',
            list: 'relative flex p-1 group',
            indicator: 'absolute transition-[translate,width] duration-200',
            trigger: [
              'group relative inline-flex items-center min-w-0 data-[state=inactive]:text-muted hover:data-[state=inactive]:not-disabled:text-default font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75',
              'transition-colors'
            ],
            leadingIcon: 'shrink-0',
            leadingAvatar: 'shrink-0',
            leadingAvatarSize: '',
            label: 'truncate',
            trailingBadge: 'shrink-0',
            trailingBadgeSize: 'sm',
            content: 'focus:outline-none w-full'
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            variant: {
              pill: {
                list: 'bg-elevated rounded-lg',
                trigger: 'grow',
                indicator: 'rounded-md shadow-xs'
              },
              link: {
                list: 'border-default',
                indicator: 'rounded-full',
                trigger: 'focus:outline-none'
              }
            },
            orientation: {
              horizontal: {
                root: 'flex-col',
                list: 'w-full',
                indicator: 'left-0 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position)',
                trigger: 'justify-center'
              },
              vertical: {
                list: 'flex-col',
                indicator: 'top-0 h-(--reka-tabs-indicator-size) translate-y-(--reka-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-primary',
                trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
              }
            },
            {
              color: 'neutral',
              variant: 'pill',
              class: {
                indicator: 'bg-inverted',
                trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
              }
            },
            {
              color: 'primary',
              variant: 'link',
              class: {
                indicator: 'bg-primary',
                trigger: 'data-[state=active]:text-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
              }
            },
            {
              color: 'neutral',
              variant: 'link',
              class: {
                indicator: 'bg-inverted',
                trigger: 'data-[state=active]:text-highlighted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            variant: 'pill',
            size: 'md'
          }
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'

export default defineConfig({
  plugins: [
    vue(),
    uiPro({
      ui: {
        tabs: {
          slots: {
            root: 'flex items-center gap-2',
            list: 'relative flex p-1 group',
            indicator: 'absolute transition-[translate,width] duration-200',
            trigger: [
              'group relative inline-flex items-center min-w-0 data-[state=inactive]:text-muted hover:data-[state=inactive]:not-disabled:text-default font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75',
              'transition-colors'
            ],
            leadingIcon: 'shrink-0',
            leadingAvatar: 'shrink-0',
            leadingAvatarSize: '',
            label: 'truncate',
            trailingBadge: 'shrink-0',
            trailingBadgeSize: 'sm',
            content: 'focus:outline-none w-full'
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            variant: {
              pill: {
                list: 'bg-elevated rounded-lg',
                trigger: 'grow',
                indicator: 'rounded-md shadow-xs'
              },
              link: {
                list: 'border-default',
                indicator: 'rounded-full',
                trigger: 'focus:outline-none'
              }
            },
            orientation: {
              horizontal: {
                root: 'flex-col',
                list: 'w-full',
                indicator: 'left-0 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position)',
                trigger: 'justify-center'
              },
              vertical: {
                list: 'flex-col',
                indicator: 'top-0 h-(--reka-tabs-indicator-size) translate-y-(--reka-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-primary',
                trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
              }
            },
            {
              color: 'neutral',
              variant: 'pill',
              class: {
                indicator: 'bg-inverted',
                trigger: 'data-[state=active]:text-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
              }
            },
            {
              color: 'primary',
              variant: 'link',
              class: {
                indicator: 'bg-primary',
                trigger: 'data-[state=active]:text-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary'
              }
            },
            {
              color: 'neutral',
              variant: 'link',
              class: {
                indicator: 'bg-inverted',
                trigger: 'data-[state=active]:text-highlighted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted'
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            variant: 'pill',
            size: 'md'
          }
        }
      }
    })
  ]
})
为了可读性,compoundVariants 中的某些颜色被省略。请查看 GitHub 上的源代码。