Tabs

Animate UI's own tabs: a highlight glides between triggers and panels slide, with the height following the active panel.

<script setup lang="ts">
import {
  Tabs,
  TabsContent,
  TabsContents,
  TabsHighlight,
  TabsHighlightItem,
  TabsList,
  TabsTrigger,
} from '@animate-ui/vue/primitives/tabs'
import { CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'

const TABS = [
  { value: 'overview', label: 'Overview' },
  { value: 'analytics', label: 'Analytics' },
  { value: 'reports', label: 'Reports' },
  { value: 'settings', label: 'Settings' },
]

const STATS = [
  ['Page views', '+25%'],
  ['Visitors', '3,240'],
  ['Bounce rate', '41%'],
  ['Avg. session', '2m 14s'],
]

const CONTENT = 'flex flex-col gap-(--card-spacing) py-(--card-spacing) [--card-spacing:--spacing(4)]'
</script>

<template>
  <Tabs default-value="overview" class="w-[400px]">
    <TabsHighlight class="absolute inset-0 z-0 rounded-md border border-transparent bg-background shadow-sm dark:border-input dark:bg-input/30">
      <TabsList class="inline-flex h-8 w-fit items-center rounded-lg bg-muted p-[3px] text-muted-foreground">
        <TabsHighlightItem v-for="tab in TABS" :key="tab.value" :value="tab.value">
          <TabsTrigger
            :value="tab.value"
            class="inline-flex h-full items-center justify-center rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-colors duration-500 ease-in-out hover:text-foreground data-[state=active]:text-foreground dark:text-muted-foreground dark:data-[state=active]:text-foreground"
          >
            {{ tab.label }}
          </TabsTrigger>
        </TabsHighlightItem>
      </TabsList>
    </TabsHighlight>
    <TabsContents class="mt-2 rounded-xl bg-card text-sm text-card-foreground ring-1 ring-foreground/10">
      <TabsContent value="overview" :class="CONTENT">
        <CardHeader>
          <CardTitle>Overview</CardTitle>
          <CardDescription>
            View your key metrics and recent project activity. Track
            progress across all your active projects.
          </CardDescription>
        </CardHeader>
        <CardContent class="text-sm text-muted-foreground">
          You have 12 active projects and 3 pending tasks.
        </CardContent>
      </TabsContent>
      <TabsContent value="analytics" :class="CONTENT">
        <CardHeader>
          <CardTitle>Analytics</CardTitle>
          <CardDescription>
            Track performance and user engagement metrics. Monitor trends
            and identify growth opportunities.
          </CardDescription>
        </CardHeader>
        <CardContent class="grid gap-2 text-sm">
          <div v-for="[label, value] in STATS" :key="label" class="flex items-center justify-between">
            <span class="text-muted-foreground">{{ label }}</span>
            <span class="font-medium tabular-nums">{{ value }}</span>
          </div>
        </CardContent>
      </TabsContent>
      <TabsContent value="reports" :class="CONTENT">
        <CardHeader>
          <CardTitle>Reports</CardTitle>
          <CardDescription>5 reports are ready to export.</CardDescription>
        </CardHeader>
      </TabsContent>
      <TabsContent value="settings" :class="CONTENT">
        <CardHeader>
          <CardTitle>Settings</CardTitle>
          <CardDescription>
            Manage your account preferences and options. Customize your
            experience to fit your needs.
          </CardDescription>
        </CardHeader>
        <CardContent class="text-sm text-muted-foreground">
          Configure notifications, security, and themes.
        </CardContent>
      </TabsContent>
    </TabsContents>
  </Tabs>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import {
  Tabs,
  TabsContent,
  TabsContents,
  TabsHighlight,
  TabsHighlightItem,
  TabsList,
  TabsTrigger,
} from '@animate-ui/vue/primitives/tabs';
</script>

<template>
  <Tabs default-value="account">
    <TabsHighlight class="absolute inset-0 rounded-md bg-background">
      <TabsList class="flex rounded-lg bg-muted p-1">
        <TabsHighlightItem value="account" class="flex-1">
          <TabsTrigger value="account">Account</TabsTrigger>
        </TabsHighlightItem>
        <TabsHighlightItem value="password" class="flex-1">
          <TabsTrigger value="password">Password</TabsTrigger>
        </TabsHighlightItem>
      </TabsList>
    </TabsHighlight>
    <TabsContents>
      <TabsContent value="account">Make changes to your account here.</TabsContent>
      <TabsContent value="password">Change your password here.</TabsContent>
    </TabsContents>
  </Tabs>
</template>

Unlike the library tabs, these are Animate UI's own: no reka-ui underneath. TabsHighlight and TabsHighlightItem are optional; without them, style the active trigger with data-state="active". Without default-value, the first trigger is selected.

Animation

  • The highlight slides to the selected trigger on a spring (stiffness 300, damping 30): one surface is handed from the previous trigger to the new one, at full opacity (Highlight).
  • Panels sit side by side and slide to the selected one on a critically damped spring (stiffness 300, damping 35): they glide into place without bouncing past it. The container's height follows the active panel on the same spring. Mounted hidden (in a closed panel, or a tab waiting its turn), the container shows at its height once it's on screen, without growing in. Inactive panels blur (4px, spring with stiffness 300, damping 30) and are inert.
  • Keyboard: arrow keys move to the next or previous enabled trigger (wrapping) and select it, Home and End jump to the ends; only the selected trigger is in the Tab order. Keyboard selection animates exactly like a click.
  • <AnimateUIProvider :tempo> paces every spring.
  • Under reduced motion, the highlight, panels and height change at once.

API Reference

Tabs

PropTypeDefault
default-value?
string-
v-model?
string-
@update:model-value?
(value: string) => void-
orientation?
'horizontal' | 'vertical''horizontal'
...props?
HTMLAttributes-

TabsHighlight

Takes the Highlight props, except controlled-items and v-model.

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 30 }

TabsHighlightItem

Takes the HighlightItem props.

PropTypeDefault
value
string-

TabsList

PropTypeDefault
...props?
HTMLAttributes-

TabsTrigger

PropTypeDefault
as-child?
booleanfalse
value
string-
...props?
HTMLMotionProps<'button'>-

TabsContents

Holds the panels and animates between them.

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 300, damping: 35, restDelta: 0.01 }
...props?
HTMLMotionProps<'div'>-

TabsContent

PropTypeDefault
as-child?
booleanfalse
value
string-
...props?
HTMLMotionProps<'div'>-

useTabs

Returns an activeValue() getter, handleValueChange, an orientation() getter, and getTriggerId / getContentId to link your own parts by id.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026