Let me try to explain. I have this motion.span element where I am animating the width of the current index to be equal to the currentTab. It animates perfectly from 0% to 100% the first time upon initialization. However, once the end is reached and it goes back to the first item of the array, I first see a FLASHING effect. For a few milliseconds, the width is at 100%, then it goes to 0% and animates back to 100%. Additionally, when I click on the LI which has a click event, somehow it jumps and loses its reference and the width of the current and previous item. Can someone please help? I’ve been trying to solve this for 3 hours now, and I’m not sure what I am doing wrong.
import { useEffect, useRef, useState } from 'react'
import { Link } from '@remix-run/react'
import { Asset, Link as CaseLink } from '#types/storyblok.js'
import clsx from 'clsx'
import { motion, useAnimation } from 'framer-motion'
import { Grid } from '#app/components/grid.tsx'
import { getImgProps } from '#app/utils/images.js'
import { Markdown } from '#app/utils/markdown.js'
import { Icon } from '../ui/icon'
import { Paragraph } from '../ui/typography'
type Case = {
_uid: string
color: string
intro: string
content: string
image: Asset
link: CaseLink
logo: Asset
tab_logo: Asset
}
type Props = {
cases: Case[]
}
export function CaseWidgetSection({ cases }: Props) {
const [selectedTab, setSelectedTab] = useState(0)
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null)
const [paused, setPaused] = useState(false)
const controls = useAnimation()
const colorSpanRefs = useRef<(HTMLSpanElement | null)[]>([])
const startAutoRotation = () => {
const newTimer = setInterval(() => {
setSelectedTab(prevTab => (prevTab + 1) % cases.length)
}, 6000)
setTimer(newTimer)
}
useEffect(() => {
startAutoRotation()
return () => {
if (timer) {
clearInterval(timer)
}
}
}, [cases])
useEffect(() => {
const handleTabChange = () => {
controls.start({
width: ['0%', '100%'],
transition: {
duration: 6,
ease: 'linear',
},
})
}
if (!paused) {
handleTabChange()
}
return () => {
if (!paused) {
controls.stop() // Stop animation when component unmounts or is paused
}
}
}, [selectedTab, paused, controls])
const handleTabClick = (index: number) => {
if (timer) {
clearInterval(timer)
}
setSelectedTab(index)
setPaused(true)
// Restart auto rotation after 6 seconds
const newTimer = setTimeout(() => {
setPaused(false)
startAutoRotation()
}, 6000)
setTimer(newTimer)
}
return (
<>
<Grid>
<div className="col-span-full">
<ul className="grid w-full grid-cols-4">
{cases.map((item, index) => {
return (
<li
key={item._uid}
className={clsx(
'group relative flex cursor-pointer place-content-center border border-b-0 border-[#DCDBF1] px-8 py-4',
index === selectedTab ? 'border-solid' : 'border-dashed',
)}
onClick={() => handleTabClick(index)}
>
<motion.span
ref={el => (colorSpanRefs.current[index] = el)}
animate={index === selectedTab ? controls : {}}
className="absolute -top-[1px] left-0 z-10 h-[1px]"
style={{
scaleX: index === selectedTab ? 1 : 0,
backgroundColor: item.color,
}}
/>
<img
src={item.tab_logo.filename}
alt={item.tab_logo.alt}
className={clsx(
'transition-all duration-200 ease-in-out',
index === selectedTab
? 'grayscale-0'
: 'grayscale group-hover:grayscale-0',
)}
/>
</li>
)
})}
</ul>
<div className="mt-8">
{cases.map((item, index) => (
<motion.div
key={item._uid}
className={`grid w-full grid-cols-12 items-start gap-x-4 ${index === selectedTab ? 'block' : 'hidden'}`}
initial="hidden"
animate={index === selectedTab ? 'visible' : 'hidden'}
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.6 } },
}}
>
<div className="col-span-4">
{item.content ? <Markdown>{item.content}</Markdown> : null}
</div>
<div className="relative col-span-8">
<Link
className="group group relative flex aspect-video h-full w-full flex-col items-center space-x-4 overflow-hidden rounded-2xl bg-black/50 object-cover"
to={`/${item.link.story?.full_slug}`}
>
<div
className="absolute left-0 top-0 z-10 h-full w-full"
style={{
backgroundImage: `radial-gradient(166.98% 89.73% at 50% 103.12%, ${item.color} 0%, ${item.color} 22.99%, rgba(254, 84, 84, 0.00) 100%), linear-gradient(0deg, rgba(0, 0, 0, 0.20) 0%, rgba(0, 0, 0, 0.20) 100%)`,
}}
/>
{item.image && (
<img
className="absolute left-0 top-0 z-0 h-full w-full transition-all duration-700 ease-in-out
group-hover:rotate-1 group-hover:scale-105"
{...getImgProps(item.image.filename, item.image.alt, {
widths: [475, 508, 1016],
sizes: [
'(max-width: 1023px) 84vw',
'(min-width: 1024px) 35vw',
'375px',
],
})}
/>
)}
<div className="absolute bottom-4 left-4 z-10 space-y-4">
{item.logo && (
<div className="relative flex">
<img
className="relative"
src={item.logo.filename}
alt={item.logo.alt}
/>
<span className="relative z-10 transition-all duration-300 ease-in-out md:relative md:-right-2 md:opacity-0 md:group-hover:-right-3 md:group-hover:opacity-100">
<Icon
name="chevron-right"
size="sm"
className="text-white"
/>
</span>
</div>
)}
{item.intro && (
<Paragraph
textColorClassName="text-white"
className="text-sm"
>
{item.intro}
</Paragraph>
)}
</div>
</Link>
</div>
</motion.div>
))}
</div>
</div>
</Grid>
</>
)
}
Nino is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.