System.Routing

Animated Tabs

A set of tab panels displayed one at a time. A faithful port of the Nuxt UI v4 tabs — same props, same slots, same theme — with an indicator that slides between triggers instead of snapping.

Installation

npx raya-ui@latest add animated-tabs

File Structure

your-project
components/ui/animated-tabs
AnimatedTabs.vue
variants.ts
index.ts

Usage

Items

Pass an array of objects. Each one becomes a trigger, and — unless `content` is `false` — a panel.

<script setup lang="ts">
import { AnimatedTabs, type TabsItem } from '@/components/ui/animated-tabs'
import { User, Lock } from '@lucide/vue'

const items: TabsItem[] = [
  { label: 'Account', icon: User, content: 'This is the account content.' },
  { label: 'Password', icon: Lock, content: 'This is the password content.' }
]
</script>

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

Content

Set `content` to `false` to render the triggers on their own — useful when the tabs drive something outside the component, like a code block header.

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

Unmount

Inactive panels are removed from the DOM by default. Set `unmount-on-hide` to `false` to keep them mounted and preserve their state.

<AnimatedTabs :unmount-on-hide="false" :items="items" class="w-full" />

Color

The color drives the indicator fill, the active label and the focus outline.

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

Variant

`pill` slides a filled rectangle behind the active trigger. `link` draws a thin line along the edge of the list.

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

Size

Sizes scale trigger padding, font size and icon size at once.

<AnimatedTabs size="xl" variant="pill" :content="false" :items="items" class="w-full" />

Orientation

Vertical tabs stack the triggers and move the indicator to the vertical edge. The panels sit beside the list.

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

Control the active item

Use `v-model` (or `default-value`) with the `value` of an item. When an item has no `value`, it is identified by its index **as a string**. Use `value-key` to match on a different key.

<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <AnimatedTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>

With route query

Drive the active tab from a URL query parameter by giving each item a `value` and binding a writable computed.

<script setup lang="ts">
const route = useRoute()
const router = useRouter()

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

const active = computed({
  get: () => (route.query.tab as string) || 'account',
  // The hash keeps the page from scrolling to the top
  set: tab => router.push({ query: { tab }, hash: '#with-route-query' })
})
</script>

<template>
  <AnimatedTabs v-model="active" :content="false" :items="items" class="w-full" />
</template>

With content slot

The `#content` slot replaces the panel of every item.

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

With custom slot

Give an item a `slot` name to render just that panel yourself.

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

<template>
  <AnimatedTabs :items="items" variant="link" :ui="{ trigger: 'grow' }" class="gap-4 w-full">
    <template #account="{ item }">
      <p class="text-muted mb-4">Make changes to your account here.</p>
    </template>

    <template #password="{ item }">
      <p class="text-muted mb-4">Change your password here.</p>
    </template>
  </AnimatedTabs>
</template>

With bottom tab bar

The `ui` prop reshapes the tabs into a mobile-style bottom bar with stacked icons and small labels.

<AnimatedTabs
  :items="items"
  :content="false"
  :ui="{
    list: 'justify-around w-full',
    trigger: 'grow flex-col gap-1 py-1',
    label: 'text-[10px]/3'
  }"
  class="w-full"
/>

Badges and avatars

An item can carry a trailing badge, and an avatar in place of an icon.

<script setup lang="ts">
const items = [
  { label: 'Inbox', icon: Inbox, badge: 12 },
  { label: 'Team', avatar: { src: 'https://github.com/nuxt.png' } }
]
</script>

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

CSS Variables

The tabs paint themselves entirely from these tokens, so they inherit your theme instead of hard-coding colors. Raw values live on :root and .dark; the @theme inline block maps them onto Tailwind's utility namespaces. That split is what lets text-inverted and bg-inverted resolve to opposite colors while both still accept opacity modifiers. This is the same set Raya Button installs — if you already have it, there is nothing new to add.

--ui-text-muted
text-muted

The resting color of an inactive trigger.

--ui-text
text-default

The color an inactive trigger takes on hover.

--ui-text-highlighted
text-highlighted

The active trigger label in the neutral link variant.

--ui-text-inverted
text-inverted

The active trigger label sitting on the neutral pill indicator.

--ui-bg-elevated
bg-elevated

The track behind the pill variant, and the badge fill.

--ui-bg-inverted
bg-inverted

The indicator fill for the neutral color.

--ui-border
border-default

The edge line of the link variant.

--ui-border-accented
ring-accented

The badge ring.

--ui-border-inverted
outline-inverted

The focus outline for the neutral color.

--ui-text-dimmed
text-dimmed

Part of the shared text scale; not painted by this component but installed with it.

--ui-text-toned
text-toned

Part of the shared text scale; not painted by this component but installed with it.

--ui-bg / --ui-bg-muted / --ui-bg-accented
bg-default / bg-muted / bg-accented

The rest of the shared surface scale.

--color-primary-content
text-primary-content

The active label on a primary pill indicator. Maps to `--primary-foreground`.

--color-secondary-content
text-secondary-content

The active label on a secondary pill indicator. Maps to `--secondary-foreground`.

--success / --success-foreground
bg-success / text-success-content

The success indicator and its active label color.

--info / --info-foreground
bg-info / text-info-content

The info indicator and its active label color.

--warning / --warning-foreground
bg-warning / text-warning-content

The warning indicator and its active label color.

--error / --error-foreground
bg-error / text-error-content

The error indicator and its active label color. Maps to `--destructive` by default.

Note: active pill labels use a per-color foreground (text-primary-content, text-success-content, …) rather than Nuxt UI's single text-inverted, matching how Raya Button handles solid variants. The neutral color still uses text-inverted, which is already correct on bg-inverted.

API Reference

Props

items
TabsItem[]
[]

The tabs to render. See the item shape below.

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

The semantic color of the indicator and the active trigger.

variant
"pill" | "link"
"pill"

A filled pill that slides behind the active trigger, or a thin line along the edge of the list.

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

Drives trigger padding, font size and icon size together.

orientation
"horizontal" | "vertical"
"horizontal"

Lays the triggers out in a row or a column, moving the indicator to the matching edge.

content
boolean
true

Set to `false` to render the triggers without any panels.

model-value
string | number
—

The value of the active tab. Bindable with `v-model`. Compared as a string.

default-value
string | number
first item

The tab active on first render, when the value is not controlled.

value-key
string
"value"

Which key identifies an item. Supports dot paths. Items without it fall back to their index as a string.

label-key
string
"label"

Which key holds the label of an item. Supports dot paths.

activation-mode
"automatic" | "manual"
"automatic"

Whether keyboard focus activates a tab immediately or only on click/Enter.

unmount-on-hide
boolean
true

Set to `false` to keep inactive panels mounted in the DOM.

as
Component | string
"div"

The element or component the root renders as.

class
string
—

Merged onto the root slot.

ui
{ root?, list?, indicator?, trigger?, leadingIcon?, leadingAvatar?, label?, trailingBadge?, content? }
—

Override the classes of any individual slot.

Item

The shape of each entry in items. Every property is optional.

label
string

The text of the trigger.

icon
Component | string

Leading icon. Accepts a component (any @lucide/vue icon) or a CSS icon class such as `i-lucide-user`.

avatar
{ src?, alt?, text?, loading? }

Leading avatar, shown when there is no icon. Falls back to `text` initials.

badge
string | number | { label?, class? }

Trailing badge, sized to the tab.

content
string

Panel text, used when no matching slot is provided.

value
string | number

Identifies the item. Defaults to its index as a string.

disabled
boolean

Prevents the tab from being activated.

slot
string

Renders this item's panel through the slot of that name.

class
string

Merged onto this item's trigger.

ui
{ trigger?, leadingIcon?, leadingAvatar?, label?, trailingBadge?, content? }

Per-item slot class overrides.

Slots

leading
{ item, index }

Replaces the leading icon or avatar of a trigger.

default
{ item, index }

Replaces the label of a trigger.

trailing
{ item, index }

Replaces the trailing badge of a trigger.

content
{ item, index }

The panel of every item that has no `slot` of its own.

#{{ item.slot }}
{ item, index }

The panel of one specific item, named by its `slot` property.

list-leading
—

Rendered inside the list, before the first trigger.

list-trailing
—

Rendered inside the list, after the last trigger.

Emits & expose

update:modelValue

Emitted with the value of the newly active tab.

triggersRef

Exposed on a template ref: the array of trigger component instances, in item order.

Theme slots

The theme lives in variants.ts as a tailwind-variants definition. Each slot can be overridden per instance with the ui prop, per item with item.ui, or globally by editing that file.

root

The wrapper around the list and the panels.

list

The trigger container — the pill background or the bordered edge.

indicator

The element that slides between triggers, positioned from the `--reka-tabs-indicator-*` variables.

trigger

Each tab button.

leadingIcon

The leading icon of a trigger.

leadingAvatar

The leading avatar image or initials.

leadingAvatarSize

The size class applied to the avatar for each tab size.

label

The `<span>` around a trigger label.

trailingBadge

The trailing badge of a trigger.

trailingBadgeSize

The size class applied to the badge.

content

Each panel.

Modify user profiles, configure identity structures, and update metadata elements within this container pane.
Settings