I'm trying to create an infinite looping animation for a list of items using React and Framer Motion. The intended sequence is:
Diagram:
The animation works, but I'm encountering visual glitches where items flash or momentarily appear in the wrong order during the loop which is affecting the smoothness of the animation.
Expected behavior: Continuous, smooth transitions between states without any flashing or incorrect ordering.
Actual behavior: Items occasionally flash or appear out of order between loops.
Code:
import React, { useEffect, useState } from "react";
import { motion, useAnimationControls } from "framer-motion";
const ITEMS_LIST = [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10",
"Item 11",
"Item 12",
];
function getItemAt(index: number) {
// Handle positive and negative indices with modulo
const adjustedIndex =
((index % ITEMS_LIST.length) + ITEMS_LIST.length) % ITEMS_LIST.length;
return ITEMS_LIST[adjustedIndex];
}
export default function App() {
const [loopIndex, setLoopIndex] = useState<number>(0);
const controlsLeftItem1 = useAnimationControls();
const controlsLeftItem2 = useAnimationControls();
const controlsLeftItem3 = useAnimationControls();
const controlsLeftItem4 = useAnimationControls();
const controlsRightItem1 = useAnimationControls();
const controlsRightItem2 = useAnimationControls();
const controlsRightItem3 = useAnimationControls();
const controlsRightItem4 = useAnimationControls();
const controlsRightItem5 = useAnimationControls();
const controlsRightItem6 = useAnimationControls();
const controlsRightItem7 = useAnimationControls();
const controlsRightItem8 = useAnimationControls();
const leftItem1 = getItemAt(loopIndex - 1);
const leftItem2 = getItemAt(loopIndex - 2);
const leftItem3 = getItemAt(loopIndex - 3);
const leftItem4 = getItemAt(loopIndex - 4);
const rightItem1 = getItemAt(loopIndex);
const rightItem2 = getItemAt(loopIndex + 1);
const rightItem3 = getItemAt(loopIndex + 2);
const rightItem4 = getItemAt(loopIndex + 3);
const rightItem5 = getItemAt(loopIndex + 4);
const rightItem6 = getItemAt(loopIndex + 5);
const rightItem7 = getItemAt(loopIndex + 6);
const rightItem8 = getItemAt(loopIndex + 7);
useEffect(() => {
Promise.all([
// Animate left column
controlsLeftItem1.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem2.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem3.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem4.start(
{ y: "calc(100% + 0.75rem)", opacity: 0 },
{ type: "spring", duration: 1 }
),
// Animate active item from right to left
controlsRightItem1.start(
{ x: "calc((100% + 122px) * -1)" },
{ type: "spring", duration: 1, delay: 0.25 }
),
// Animate right column
controlsRightItem2.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem3.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem4.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem5.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem6.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem7.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem8.start(
{ y: "calc((100% + 0.75rem) * -1)", opacity: 1 },
{ type: "spring", duration: 1, delay: 1 }
),
]).then(() => {
window.setTimeout(() => {
setLoopIndex((old) => old + 1);
window.setTimeout(() => {
controlsLeftItem1.set({ y: "calc(0% + 0rem)" });
controlsLeftItem2.set({ y: "calc(0% + 0rem)" });
controlsLeftItem3.set({ y: "calc(0% + 0rem)" });
controlsLeftItem4.set({ y: "calc(0% + 0rem)", opacity: 1 });
controlsRightItem1.set({ x: "calc((0% + 0px) * -1)" });
controlsRightItem2.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem3.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem4.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem5.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem6.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem7.set({ y: "calc((0% + 0rem) * -1)" });
controlsRightItem8.set({ y: "calc((0% + 0rem) * -1)", opacity: 0 });
});
}, 1000);
});
}, [loopIndex]);
return (
<div className="relative w-[670px] pb-[3.75rem] pt-3">
<div className="absolute right-0 top-0 bg-red-300 text-white">
{loopIndex}
</div>
<div className="relative">
<div className="absolute inset-0 z-0 rounded-3xl border-[5px] border-white/20 bg-white/35" />
<div className="relative z-10 flex justify-between pr-6">
<ul className="flex flex-col gap-3 p-7">
<li className="pb-5 opacity-50">
<Item label="Static 1" />
</li>
<motion.li animate={controlsLeftItem1}>
<Item label={leftItem1} />
</motion.li>
<motion.li animate={controlsLeftItem2}>
<Item label={leftItem2} />
</motion.li>
<motion.li animate={controlsLeftItem3}>
<Item label={leftItem3} />
</motion.li>
<motion.li animate={controlsLeftItem4}>
<Item label={leftItem4} />
</motion.li>
<li className="pt-5 opacity-50">
<Item label="Static 2" />
</li>
<li className="opacity-50">
<Item label="Static 3" />
</li>
</ul>
<ul className="-mb-20 -mt-3 flex flex-col gap-3">
<li className="opacity-50">
<Item label="Static 1" />
</li>
<li className="opacity-50">
<Item label="Static 2" />
</li>
<motion.li animate={controlsRightItem1}>
<Item label={rightItem1} />
</motion.li>
<motion.li animate={controlsRightItem2}>
<Item label={rightItem2} />
</motion.li>
<motion.li animate={controlsRightItem3}>
<Item label={rightItem3} />
</motion.li>
<motion.li animate={controlsRightItem4}>
<Item label={rightItem4} />
</motion.li>
<motion.li animate={controlsRightItem5}>
<Item label={rightItem5} />
</motion.li>
<motion.li animate={controlsRightItem6}>
<Item label={rightItem6} />
</motion.li>
<motion.li animate={controlsRightItem7}>
<Item label={rightItem7} />
</motion.li>
<motion.li animate={controlsRightItem8}>
<Item label={rightItem8} />
</motion.li>
</ul>
</div>
</div>
</div>
);
}
function Item({ label }: { label: string }) {
return (
<div className="flex h-12 w-[248px] flex-shrink-0 flex-grow-0 items-center gap-4 rounded-xl bg-white/50 px-4">
<span className="flex-shrink-0 flex-grow-0">
<div className="w-6 h-6 rounded-full bg-white" />
</span>
<span className="flex-1 text-sm font-medium uppercase leading-none tracking-widest">
{label}
</span>
</div>
);
}
You can see the issue in action here:
Here's a link to the CodeSandbox:
https://codesandbox.io/p/sandbox/quizzical-heisenberg-lyr7ct
Questions:
Any advice would be greatly appreciated! I'm relatively new to React, so if there are more "React-like" approaches to handle this, please tell me!
Solved!
I fixed the glitches by:
useAnimationControls.set({...})
and moving values to the <motion.li initial={{...}}>
prop.loopIndex
state gets updated e.g. <motion.li key={`left-${loopIndex}-1`}>
useEffect
.Here's the resulting code:
import React, { useEffect, useState } from "react";
import { motion, useAnimationControls } from "framer-motion";
const ITEMS_LIST = [
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9",
"Item 10",
"Item 11",
"Item 12",
];
function getItemAt(index: number) {
// Handle positive and negative indices with modulo
const adjustedIndex =
((index % ITEMS_LIST.length) + ITEMS_LIST.length) % ITEMS_LIST.length;
return ITEMS_LIST[adjustedIndex];
}
export default function App() {
const [loopIndex, setLoopIndex] = useState<number>(0);
const controlsLeftItem1 = useAnimationControls();
const controlsLeftItem2 = useAnimationControls();
const controlsLeftItem3 = useAnimationControls();
const controlsLeftItem4 = useAnimationControls();
const controlsRightItem1 = useAnimationControls();
const controlsRightItem2 = useAnimationControls();
const controlsRightItem3 = useAnimationControls();
const controlsRightItem4 = useAnimationControls();
const controlsRightItem5 = useAnimationControls();
const controlsRightItem6 = useAnimationControls();
const controlsRightItem7 = useAnimationControls();
const controlsRightItem8 = useAnimationControls();
const leftItem1 = getItemAt(loopIndex - 1);
const leftItem2 = getItemAt(loopIndex - 2);
const leftItem3 = getItemAt(loopIndex - 3);
const leftItem4 = getItemAt(loopIndex - 4);
const rightItem1 = getItemAt(loopIndex);
const rightItem2 = getItemAt(loopIndex + 1);
const rightItem3 = getItemAt(loopIndex + 2);
const rightItem4 = getItemAt(loopIndex + 3);
const rightItem5 = getItemAt(loopIndex + 4);
const rightItem6 = getItemAt(loopIndex + 5);
const rightItem7 = getItemAt(loopIndex + 6);
const rightItem8 = getItemAt(loopIndex + 7);
useEffect(() => {
let timeout: null | number = null;
Promise.all([
// Animate left column
controlsLeftItem1.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem2.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem3.start(
{ y: "calc(100% + 0.75rem)" },
{ type: "spring", duration: 1 }
),
controlsLeftItem4.start(
{ y: "calc(100% + 0.75rem)", opacity: 0 },
{ type: "spring", duration: 1 }
),
// Animate active item from right to left
controlsRightItem1.start(
{ x: "calc((100% + 122px) * -1)" },
{ type: "spring", duration: 1, delay: 0.25 }
),
// Animate right column
controlsRightItem2.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem3.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem4.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem5.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem6.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem7.start(
{ y: "calc((100% + 0.75rem) * -1)" },
{ type: "spring", duration: 1, delay: 0.5 }
),
controlsRightItem8.start(
{ y: "calc((100% + 0.75rem) * -1)", opacity: 1 },
{ type: "spring", duration: 1, delay: 0.5 }
),
]).then(() => {
timeout = window.setTimeout(() => {
setLoopIndex((old) => old + 1);
}, 1000);
});
return () => {
controlsLeftItem1.stop();
controlsLeftItem2.stop();
controlsLeftItem3.stop();
controlsLeftItem4.stop();
controlsRightItem1.stop();
controlsRightItem2.stop();
controlsRightItem3.stop();
controlsRightItem4.stop();
controlsRightItem5.stop();
controlsRightItem6.stop();
controlsRightItem7.stop();
controlsRightItem8.stop();
if (timeout !== null) window.clearTimeout(timeout);
};
}, [loopIndex]);
return (
<div className="relative w-[670px] pb-[3.75rem] pt-3">
<div className="absolute right-0 top-0 bg-red-300 text-white">
{loopIndex}
</div>
<div className="relative">
<div className="absolute inset-0 z-0 rounded-3xl border-[5px] border-white/20 bg-white/35" />
<div className="relative z-10 flex justify-between pr-6">
<ul className="flex flex-col gap-3 p-7">
<li className="pb-5 opacity-50">
<Item label="Static 1" />
</li>
<motion.li
key={`left-${loopIndex}-1`}
animate={controlsLeftItem1}
initial={{ y: "calc(0% + 0rem)" }}
>
<Item label={leftItem1} />
</motion.li>
<motion.li
key={`left-${loopIndex}-2`}
animate={controlsLeftItem2}
initial={{ y: "calc(0% + 0rem)" }}
>
<Item label={leftItem2} />
</motion.li>
<motion.li
key={`left-${loopIndex}-3`}
animate={controlsLeftItem3}
initial={{ y: "calc(0% + 0rem)" }}
>
<Item label={leftItem3} />
</motion.li>
<motion.li
key={`left-${loopIndex}-4`}
animate={controlsLeftItem4}
initial={{ y: "calc(0% + 0rem)", opacity: 1 }}
>
<Item label={leftItem4} />
</motion.li>
<li className="pt-5 opacity-50">
<Item label="Static 2" />
</li>
<li className="opacity-50">
<Item label="Static 3" />
</li>
</ul>
<ul className="-mb-20 -mt-3 flex flex-col gap-3">
<li className="opacity-50">
<Item label="Static 1" />
</li>
<li className="opacity-50">
<Item label="Static 2" />
</li>
<motion.li
key={`right-${loopIndex}-1`}
animate={controlsRightItem1}
initial={{ x: "calc((0% + 0px) * -1)" }}
>
<Item label={rightItem1} />
</motion.li>
<motion.li
key={`right-${loopIndex}-2`}
animate={controlsRightItem2}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem2} />
</motion.li>
<motion.li
key={`right-${loopIndex}-3`}
animate={controlsRightItem3}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem3} />
</motion.li>
<motion.li
key={`right-${loopIndex}-4`}
animate={controlsRightItem4}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem4} />
</motion.li>
<motion.li
key={`right-${loopIndex}-5`}
animate={controlsRightItem5}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem5} />
</motion.li>
<motion.li
key={`right-${loopIndex}-6`}
animate={controlsRightItem6}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem6} />
</motion.li>
<motion.li
key={`right-${loopIndex}-7`}
animate={controlsRightItem7}
initial={{ y: "calc((0% + 0rem) * -1)" }}
>
<Item label={rightItem7} />
</motion.li>
<motion.li
key={`right-${loopIndex}-8`}
animate={controlsRightItem8}
initial={{ y: "calc((0% + 0rem) * -1)", opacity: 0 }}
>
<Item label={rightItem8} />
</motion.li>
</ul>
</div>
</div>
</div>
);
}
function Item({ label }: { label: string }) {
return (
<div className="flex h-12 w-[248px] flex-shrink-0 flex-grow-0 items-center gap-4 rounded-xl bg-white/50 px-4">
<span className="flex-shrink-0 flex-grow-0">
<div className="w-6 h-6 rounded-full bg-white" />
</span>
<span className="flex-1 text-sm font-medium uppercase leading-none tracking-widest">
{label}
</span>
</div>
);
}
You can see the result here:
And here's a link to the updated CodeSandbox:
https://codesandbox.io/p/sandbox/react-animation-fixed-t6r38c