Transition Panel
An animated panel that transitions between different content steps with enter and exit animations. Inspired by Motion Primitives’ Transition Panel . Perfect for step-based card flows and onboarding wizards.
Installation
npx shadcn@latest add https://prismaui.com/components/transition-panel.json
Import
import { TransitionPanel } from '@/components/ui/transition-panel';Card with Steps
Use TransitionPanel inside a Card to create a step-based flow. Control the activeIndex externally.
Brand
Develop a distinctive brand identity with tailored logos and guidelines to ensure consistent messaging across all platforms.
'use client';
import { useState } from 'react';
import { TransitionPanel } from '@/components/ui/transition-panel';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
const steps = [
{
title: 'Brand',
description:
'Develop a distinctive brand identity with tailored logos and guidelines.',
},
{
title: 'Product',
description:
'Design intuitive product interfaces that delight users and drive engagement.',
},
{
title: 'Launch',
description:
'Execute a strategic launch with coordinated marketing and communications.',
},
];
export default function StepCard() {
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const handlePrev = () => {
setDirection(-1);
setActiveIndex((prev) => Math.max(prev - 1, 0));
};
const handleNext = () => {
setDirection(1);
setActiveIndex((prev) => Math.min(prev + 1, steps.length - 1));
};
return (
<Card className='w-85'>
<CardContent>
<TransitionPanel activeIndex={activeIndex} custom={direction}>
{steps.map((step) => (
<div key={step.title}>
<h3 className='mb-2 text-lg font-medium'>{step.title}</h3>
<p className='text-sm text-muted-foreground'>
{step.description}
</p>
</div>
))}
</TransitionPanel>
</CardContent>
<CardFooter className='flex justify-between'>
<Button
variant='outline'
size='sm'
onClick={handlePrev}
disabled={activeIndex === 0}
>
Previous
</Button>
<Button
size='sm'
onClick={handleNext}
disabled={activeIndex === steps.length - 1}
>
Next
</Button>
</CardFooter>
</Card>
);
}Delete Confirmation Modal
A two-step modal flow using Dialog: first a warning that the action is irreversible, then a confirmation step with the delete button.
'use client';
import { useState } from 'react';
import { TransitionPanel } from '@/components/ui/transition-panel';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export function DeleteUserModal() {
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const [open, setOpen] = useState(false);
return (
<Dialog
open={open}
onOpenChange={(v) => {
setOpen(v);
if (!v) setActiveIndex(0);
}}
>
<DialogTrigger asChild>
<Button variant='destructive'>Delete Account</Button>
</DialogTrigger>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>Delete Account</DialogTitle>
<DialogDescription>
{activeIndex === 0
? 'Please review before proceeding.'
: 'Step 2 of 2'}
</DialogDescription>
</DialogHeader>
<TransitionPanel activeIndex={activeIndex} custom={direction}>
{[
<div key='warning'>
<div className='mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 text-destructive'>
⚠
</div>
<span className='text-sm text-muted-foreground'>
This action is <strong>permanent and irreversible</strong>. All
your data will be permanently deleted and cannot be recovered.
</span>
</div>,
<div key='confirm'>
<span className='mb-3 block text-sm text-muted-foreground'>
Click the button below to confirm deletion.
</span>
<div className='rounded-md border border-destructive/20 bg-destructive/5 p-3'>
<span className='text-xs text-destructive'>
You are about to delete user <strong>john@example.com</strong>
. This cannot be undone.
</span>
</div>
</div>,
]}
</TransitionPanel>
<DialogFooter className='flex-row justify-between sm:justify-between'>
{activeIndex === 0 ? (
<>
<Button
variant='outline'
size='sm'
onClick={() => setOpen(false)}
>
Cancel
</Button>
<Button
variant='destructive'
size='sm'
onClick={() => {
setDirection(1);
setActiveIndex(1);
}}
>
Continue
</Button>
</>
) : (
<>
<Button
variant='outline'
size='sm'
onClick={() => {
setDirection(-1);
setActiveIndex(0);
}}
>
Go Back
</Button>
<Button
variant='destructive'
size='sm'
onClick={() => {
setOpen(false);
setActiveIndex(0);
}}
>
Delete Account
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode[] | — | Array of content to transition between. |
activeIndex | number | — | Index of the currently displayed child. |
className | string | — | CSS class for the container. |
transition | Transition | — | Motion transition config. |
variants | { enter, center, exit } | — | Custom animation variants. |
Last updated on