Components
Loading preview...
Easy way to switch between different pieces of content with enter and exit animations. It's perfect for enhancing user experience in areas such as onboarding card, settings adjustments, or any interactive content that requires a visual transition between states.
@motion-primitives
npx shadcn@latest add https://21st.dev/r/ibelick/transition-panel"use client";
import React, { useEffect, useState } from "react";
import { TransitionPanel } from "@/components/ui/transition-panel";
import useMeasure from "react-use-measure";
function Button({
onClick,
children,
}: {
onClick: () => void,
children: React.ReactNode,
}) {
return (
<button
onClick={onClick}
type="button"
className="relative flex h-8 shrink-0 scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 bg-transparent px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98] dark:border-zinc-50/10 dark:text-zinc-50 dark:hover:bg-zinc-800"
>
{children}
</button>
);
}
export function TransitionPanelCard() {
const [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const [ref, bounds] = useMeasure();
const FEATURES = [
{
title: "Brand",
description:
"Develop a distinctive brand identity with tailored logos and guidelines to ensure consistent messaging across all platforms.",
},
{
title: "Product",
description:
"Design and refine products that excel in user experience, meeting needs effectively and creating memorable interactions. We specialize in web applications.",
},
{
title: "Website",
description:
"Create impactful websites that combine beautiful aesthetics with functional design, ensuring a superior online presence.",
},
{
title: "Design System",
description:
"Develop a design system that unifies your brand identity, ensuring consistency across all platforms and products.",
},
];
const handleSetActiveIndex = (newIndex: number) => {
setDirection(newIndex > activeIndex ? 1 : -1);
setActiveIndex(newIndex);
};
useEffect(() => {
if (activeIndex < 0) setActiveIndex(0);
if (activeIndex >= FEATURES.length) setActiveIndex(FEATURES.length - 1);
}, [activeIndex]);
const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 364 : -364,
opacity: 0,
}),
center: {
zIndex: 1,
x: 0,
opacity: 1,
},
exit: (direction: number) => ({
zIndex: 0,
x: direction < 0 ? 364 : -364,
opacity: 0,
position: "absolute",
top: 0,
left: 0,
width: "100%",
}),
};
return (
<div className="w-[364px] overflow-hidden rounded-xl border border-zinc-950/10 bg-white dark:bg-zinc-700">
<TransitionPanel
activeIndex={activeIndex}
variants={{
enter: (direction) => ({
x: direction > 0 ? 364 : -364,
opacity: 0,
height: bounds.height > 0 ? bounds.height : "auto",
position: "initial",
}),
center: {
zIndex: 1,
x: 0,
opacity: 1,
height: bounds.height > 0 ? bounds.height : "auto",
},
exit: (direction) => ({
zIndex: 0,
x: direction < 0 ? 364 : -364,
opacity: 0,
position: "absolute",
top: 0,
width: "100%",
}),
}}
transition={{
x: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
custom={direction}
>
{FEATURES.map((feature, index) => (
<div key={index} className="px-4 pt-4" ref={ref}>
<h3 className="mb-0.5 font-medium text-zinc-800 dark:text-zinc-100">
{feature.title}
</h3>
<p className="text-zinc-600 dark:text-zinc-400">
{feature.description}
</p>
</div>
))}
</TransitionPanel>
<div className="flex justify-between p-4">
{activeIndex > 0 ? (
<Button onClick={() => handleSetActiveIndex(activeIndex - 1)}>
Previous
</Button>
) : (
<div />
)}
<Button
onClick={() =>
activeIndex === FEATURES.length - 1
? null
: handleSetActiveIndex(activeIndex + 1)
}
>
{activeIndex === FEATURES.length - 1 ? "Close" : "Next"}
</Button>
</div>
</div>
);
}