Transition Panel
A panel that animates its content between states. Give each piece of content a key and an index; the panel animates the one leaving out and the one arriving in while pushing the rest of the layout to make room.
<AnimatePresence :initial="false" mode="popLayout">,
so content does not animate on first paint — only on a change of
activeIndex.Usage
activeIndex selects which child is in view. Everything else is your own
content, keyed and switched on the same value.
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<MTransitionPanel
:active-index="active"
:variants="variants"
:transition="{ duration: 0.35, ease: [0.32, 0.72, 0, 1] }"
class="overflow-hidden rounded-2xl"
>
<div
:key="active"
class="flex h-40 flex-col items-center justify-center gap-1 p-6 text-center"
:class="panels[active].tone"
>
<span class="text-xs font-medium uppercase tracking-widest opacity-70">
{{ panels[active].eyebrow }}
</span>
<span class="text-lg font-semibold text-white">
{{ panels[active].title }}
</span>
</div>
</MTransitionPanel>
<div class="grid grid-cols-3 gap-2">
<UButton
v-for="(panel, index) in panels"
:key="panel.title"
:label="String(index + 1)"
:variant="index === active ? 'solid' : 'outline'"
class="justify-center"
@click="active = index"
/>
</div>
</div>
</template>
<script setup lang="ts">
const active = ref(1)
const panels = [
{ eyebrow: 'Queued', title: 'Waiting on CI', tone: 'bg-primary' },
{ eyebrow: 'Running', title: 'Building bundle', tone: 'bg-amber-600' },
{ eyebrow: 'Done', title: 'Deployed to prod', tone: 'bg-emerald-600' },
]
// The panel is keyed on `activeIndex` internally, so the slot only has to supply
// the content for the index currently on screen.
const variants = {
enter: { opacity: 0, y: 24, rotateX: -12 },
center: { opacity: 1, y: 0, rotateX: 0 },
exit: { opacity: 0, y: -24, rotateX: 12 },
}
</script>
The default slot holds a single child: the content for the index currently on
screen. The panel keys that child on activeIndex internally, so the slot only
has to supply the current content — it does not key or switch children itself.
Examples
Directional exit
Pass a custom value and read it from a function-valued exit variant to send
the outgoing item in a direction. custom from the AnimatePresence reaches
the exiting child, so the variant can decide where the piece leaves — here,
direction is -1 or 1 and the item slides out on that side.
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<MTransitionPanel
:active-index="active"
:custom="direction"
:variants="variants"
:transition="{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }"
class="overflow-hidden rounded-2xl"
>
<div
:key="active"
class="flex h-40 flex-col items-center justify-center gap-1 p-6 text-center"
:class="steps[active].tone"
>
<span class="text-xs font-medium uppercase tracking-widest text-white/70">
{{ steps[active].eyebrow }}
</span>
<span class="text-lg font-semibold text-white">
{{ steps[active].title }}
</span>
</div>
</MTransitionPanel>
<div class="flex items-center justify-between">
<UButton
label="Back"
icon="i-lucide-arrow-left"
variant="outline"
:disabled="active === 0"
@click="go(-1)"
/>
<span class="text-xs text-muted">{{ active + 1 }} / {{ steps.length }}</span>
<UButton
label="Next"
trailing-icon="i-lucide-arrow-right"
:disabled="active === steps.length - 1"
@click="go(1)"
/>
</div>
</div>
</template>
<script setup lang="ts">
const active = ref(1)
const direction = ref(1)
// `custom` reaches the exiting child, so only `exit` can vary with direction:
// the outgoing panel leaves opposite the way the visitor is moving, while the
// arriving panel always enters from the same side.
const variants = {
enter: { opacity: 0, x: 48 },
center: { opacity: 1, x: 0 },
exit: (dir: number) => ({ opacity: 0, x: dir * -48 }),
}
const steps = [
{ eyebrow: 'Step one', title: 'Connect your account', tone: 'bg-primary' },
{ eyebrow: 'Step two', title: 'Invite your team', tone: 'bg-amber-600' },
{ eyebrow: 'Step three', title: 'Ship your first change', tone: 'bg-emerald-600' },
]
function go(step: number) {
direction.value = step
active.value = Math.min(Math.max(active.value + step, 0), steps.length - 1)
}
</script>
Customize
Adjust the active index and replay live in the playground below. custom is not
a control here because its shape is yours to define; see the directional exit
example.
Panel content
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
activeIndex | number | - | The index of the child currently in view. Changing it is what plays the transition. |
variants | Variants | - | The enter, center and exit variants applied to the active child. enter
and center are resolved as-is; exit may be a function that receives
custom. |
transition | Transition | - | The motion-v transition applied to the variant change. |
custom | any | - | A value passed to the AnimatePresence and read by a function-valued exit
variant. Use it to vary the exit by direction or by any other state. |
class | string | - | Merged onto the root. |
ui | object | undefined | Per-slot classes for the theme. Only the root slot is defined; see
UI. |
Slots
| Slot | Props | Description |
|---|---|---|
default | - | The content for the index currently on screen. The panel keys it internally. |
UI
This component's theme exposes the slot below. Override it per instance with the ui prop, or app-wide under ui.transitionPanel in app.config.ts.
| Slot | Props | Description |
|---|---|---|
root | - | relative |
Accessibility
The panel animates layout, so it can move content that has focus. Keep focus on
the control that changed the step, or move it deliberately after the
transition — popLayout keeps the leaving item in flow until it is finished, so
the tab order does not jump mid-animation.
Under prefers-reduced-motion motion-v skips the animation and the change
applies immediately.
Performance
mode="popLayout"takes the leaving child out of flow, so the arriving child can animate into place without the container collapsing.:initial="false"on theAnimatePresencemeans the first child does not animate on mount — only a change ofactiveIndexplays a transition.- Variants resolve per child; keeping
enterandcenteras plain objects (and onlyexitas a function ofcustom) avoids resolving functions repeatedly for the arriving child.
Shader Reveal Transition
Eight reveal shapes — from a noise gate to a barcode shutter — in one compiled shader.
Composables
Composables are reusable functions that encapsulate reactive logic and can be shared across components. They follow the Vue 3 Composition API pattern and provide a clean way to organize and share logic in your Nuxt application.