But this brings a new problem: because the child element is very long, the position of the initial state 100%
is actually very far away from the viewport, causing it to take some time before the element is visible after starting the animation.
To solve this problem:
1. Use useRef
Use useRef
to get the actual width of motion.div
and its parent div
(by ref.current?.clientWidth
), then animate x
from -widthOfDiv
to widthOfMotionDiv
.
But useRef
is initialized after motion, so this is actually very cumbersome and I won't expand on it here.
2. Use absolute
By using an absolute
layout and animating translateX
and left
at the same time, it's easy to move an element completely from right to left. The downside is that you need to specify the height
of the parent container in advance:
function SkillList() {
const technologies = [
{ icon: <div className="h-16 w-16 bg-red-400" />, name: "HTML" },
{ icon: <div className="h-16 w-16 bg-orange-400" />, name: "CSS" },
{ icon: <div className="h-16 w-16 bg-amber-400" />, name: "TailwindCSS" },
{ icon: <div className="h-16 w-16 bg-yellow-400" />, name: "JavaScript" },
{ icon: <div className="h-16 w-16 bg-lime-400" />, name: "React.js" },
{ icon: <div className="h-16 w-16 bg-green-400" />, name: "Next.js" },
{ icon: <div className="h-16 w-16 bg-emerald-400" />, name: "Python" },
{ icon: <div className="h-16 w-16 bg-teal-400" />, name: "Django" },
{ icon: <div className="h-16 w-16 bg-cyan-400" />, name: "MongoDB" },
{ icon: <div className="h-16 w-16 bg-sky-400" />, name: "AWS" },
];
const motion = window['Motion'].motion;
return (
<div className="max-w-5xl mx-auto">
<section className="py-12">
<div className="container mx-auto">
<h2 className="mb-8 text-center text-3xl font-bold">
Technologies I Know
</h2>
<div className="relative h-32 overflow-hidden">
<motion.div
className="absolute flex w-fit gap-2 whitespace-nowrap"
initial={{ x: "0%", left: "100%" }}
animate={{ x: "-100%", left: "0%" }}
transition={{ duration: 10, ease: "linear", repeat: Infinity }}
>
{technologies.map((tech, index) => (
<div
key={index}
className="flex h-32 w-32 flex-shrink-0 flex-col items-center p-2"
>
<div className="text-accent text-4xl">{tech.icon}</div>
<span className="text-center text-lg">{tech.name}</span>
</div>
))}
{technologies.map((tech, index) => (
<div
key={index + technologies.length}
className="flex h-32 w-32 flex-shrink-0 flex-col items-center p-2"
>
<div className="text-accent text-4xl">{tech.icon}</div>
<span className="text-center text-lg">{tech.name}</span>
</div>
))}
</motion.div>
</div>
</div>
</section>
</div>
);
}
ReactDOM.createRoot(document.getElementById('app')).render(<SkillList/>);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>
<script src="https://cdn.jsdelivr.net/npm/framer-motion@11.3.8/dist/framer-motion.min.js"></script>
<div id="app"></div>
3. Infinite scrolling
Infinite scrolling changes the end result from full right-to-left scrolling, but it's still visually appealing. You just need to make sure that the list width
is greater than the container width
:
function SkillList() {
const motion = window['Motion'].motion;
return (
<div className="mx-auto max-w-5xl">
<section className="py-12">
<div className="container mx-auto">
<h2 className="mb-8 text-center text-3xl font-bold">
Technologies I Know
</h2>
<div className="overflow-hidden">
<motion.div
className="relative w-fit"
initial={{ x: "0%" }}
animate={{ x: "-100%" }}
transition={{ duration: 5, ease: "linear", repeat: Infinity }}
>
<List />
<div className="absolute left-full top-0">
<List />
</div>
</motion.div>
</div>
</div>
</section>
</div>
);
}
function List() {
const technologies = [
{ icon: <div className="h-16 w-16 bg-red-400" />, name: "HTML" },
{ icon: <div className="h-16 w-16 bg-orange-400" />, name: "CSS" },
{ icon: <div className="h-16 w-16 bg-amber-400" />, name: "TailwindCSS" },
{ icon: <div className="h-16 w-16 bg-yellow-400" />, name: "JavaScript" },
{ icon: <div className="h-16 w-16 bg-lime-400" />, name: "React.js" },
{ icon: <div className="h-16 w-16 bg-green-400" />, name: "Next.js" },
{ icon: <div className="h-16 w-16 bg-emerald-400" />, name: "Python" },
{ icon: <div className="h-16 w-16 bg-teal-400" />, name: "Django" },
{ icon: <div className="h-16 w-16 bg-cyan-400" />, name: "MongoDB" },
{ icon: <div className="h-16 w-16 bg-sky-400" />, name: "AWS" },
];
return (
<div className="flex gap-2 whitespace-nowrap">
{technologies.map((tech, index) => (
<div
key={index}
className="flex h-32 w-32 flex-shrink-0 flex-col items-center p-2"
>
<div className="text-accent text-4xl">{tech.icon}</div>
<span className="text-center text-lg">{tech.name}</span>
</div>
))}
</div>
);
}
ReactDOM.createRoot(document.getElementById('app')).render(<SkillList/>);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>
<script src="https://cdn.jsdelivr.net/npm/framer-motion@11.3.8/dist/framer-motion.min.js"></script>
<div id="app"></div>