feat: add animated transitions to cloud features tabs

- Implemented slide and fade animations when switching between feature tabs using Framer Motion
- Added animated background indicator that follows the selected tab
- Enhanced hover states with scale transitions and outline effects for better interactivity
This commit is contained in:
2025-11-06 21:40:26 +01:00
parent 2e22ed9683
commit e7b33b75c9

View File

@@ -130,27 +130,111 @@ const features = [
] ]
interface CustomAnimationProps {
isForwards: boolean
changeCount: number
}
const maxZIndex = 2147483647
const bodyVariantBackwards: Variant = {
opacity: 0.4,
scale: 0.8,
zIndex: 0,
filter: 'blur(4px)',
transition: { duration: 0.4 },
}
const bodyAnimation: MotionProps = {
initial: 'initial',
animate: 'animate',
exit: 'exit',
variants: {
initial: (custom: CustomAnimationProps) =>
custom.isForwards
? {
y: '100%',
zIndex: maxZIndex - custom.changeCount,
transition: { duration: 0.4 },
}
: bodyVariantBackwards,
animate: (custom: CustomAnimationProps) => ({
y: '0%',
opacity: 1,
scale: 1,
zIndex: maxZIndex / 2 - custom.changeCount,
filter: 'blur(0px)',
transition: { duration: 0.4 },
}),
exit: (custom: CustomAnimationProps) =>
custom.isForwards
? bodyVariantBackwards
: {
y: '100%',
zIndex: maxZIndex - custom.changeCount,
transition: { duration: 0.4 },
},
},
}
function usePrevious<T>(value: T) {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
/* Desktop Component */ /* Desktop Component */
function CloudFeaturesDesktop() { function CloudFeaturesDesktop() {
const [selectedIndex, setSelectedIndex] = useState(0) let [changeCount, setChangeCount] = useState(0)
let [selectedIndex, setSelectedIndex] = useState(0)
let prevIndex = usePrevious(selectedIndex)
let isForwards = prevIndex === undefined ? true : selectedIndex > prevIndex
let onChange = useDebouncedCallback(
(selectedIndex: number) => {
setSelectedIndex(selectedIndex)
setChangeCount((changeCount) => changeCount + 1)
},
100,
{ leading: true },
)
return ( return (
<TabGroup vertical className="grid grid-cols-12 gap-10"> <TabGroup
vertical
className="grid grid-cols-12 items-start gap-10"
selectedIndex={selectedIndex}
onChange={onChange}
>
<TabList className="col-span-6 space-y-6 pl-4 sm:pl-6 lg:pl-8"> <TabList className="col-span-6 space-y-6 pl-4 sm:pl-6 lg:pl-8">
{features.map((feature, i) => ( {features.map((feature, featureIndex) => (
<div <div
key={feature.name} key={feature.name}
className={clsx( className={clsx(
'relative rounded-2xl transition-all hover:scale-[1.02] hover:bg-gray-100', 'relative rounded-2xl outline-2 transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-100',
selectedIndex === i selectedIndex === featureIndex
? 'ring-2 ring-cyan-500 bg-white shadow' ? 'outline-cyan-500'
: 'ring-1 ring-transparent', : 'outline-transparent hover:outline-cyan-500',
)} )}
> >
<div className="p-8"> {featureIndex === selectedIndex && (
<motion.div
layoutId="activeBackground"
className="absolute inset-0 bg-white shadow"
initial={{ borderRadius: 16 }}
/>
)}
<div className="relative z-10 p-8">
<feature.icon className="h-8 w-8" /> <feature.icon className="h-8 w-8" />
<FeatureTitle as="h3" className="mt-6 text-gray-900"> <FeatureTitle as="h3" className="mt-6 text-gray-900">
<Tab>{feature.name}</Tab> <Tab className="text-left data-selected:not-data-focus:outline-hidden">
<span className="absolute inset-0 rounded-2xl" />
{feature.name}
</Tab>
</FeatureTitle> </FeatureTitle>
<FeatureDescription className="mt-2 text-gray-600"> <FeatureDescription className="mt-2 text-gray-600">
{feature.description} {feature.description}
@@ -160,15 +244,30 @@ function CloudFeaturesDesktop() {
))} ))}
</TabList> </TabList>
<div className="col-span-6"> <div className="relative col-span-6">
<TabPanels as={Fragment}> <TabPanels as={Fragment}>
{features.map((feature, i) => ( <AnimatePresence
<TabPanel key={feature.name}> initial={false}
<div className="w-full flex justify-center"> custom={{ isForwards, changeCount }}
>
{features.map((feature, featureIndex) =>
selectedIndex === featureIndex ? (
<TabPanel
static
key={feature.name + changeCount}
className="col-start-1 row-start-1 flex focus:outline-offset-32 data-selected:not-data-focus:outline-hidden"
>
<motion.div
{...bodyAnimation}
custom={{ isForwards, changeCount }}
className="w-full flex justify-center"
>
<feature.screen /> <feature.screen />
</div> </motion.div>
</TabPanel> </TabPanel>
))} ) : null,
)}
</AnimatePresence>
</TabPanels> </TabPanels>
</div> </div>
</TabGroup> </TabGroup>