While redesigning the homepage of ObsidianStats.com, I stumbled upon a challenge. The “Trending Plugins” section with a bunch of cards was visually blending into the rest of the page. Similar card layouts were already used in other sections, and frankly, it started to feel repetitive.
I knew I needed something subtle but dynamic — something that draws the user’s eye naturally without shouting for attention. I considered typical solutions like sliders, carousels, and even dynamic grids. But somehow, they either felt too cliché or disturbed the reading flow I had carefully built for the homepage.
That’s when the idea of an infinite horizontal scroll hit me.
When you want a section to feel alive but not overpower the rest of the page, infinite scrolling is a magic trick. It makes content dynamic, yet lets the user focus on their own pace.
Plus, it’s lightweight when done right. That was important because at ObsidianStats, I always try to keep loading times snappy.
First, I needed the basics: a container that holds items in a straight horizontal line. Simple flex
and overflow: hidden
CSS did the trick:
<div style={{ display: 'flex', overflow: 'hidden', whiteSpace: 'nowrap' }}>
{/* Scrolling content */}
</div>
Then came the real fun part — animating the scroll.
Instead of complex timers, I used useAnimationFrame
from Framer Motion. It updates smoothly every frame, keeping motion consistent even on low-end devices:
const [offsetX, setOffsetX] = useState(0);
const speed = 0.05;
useAnimationFrame((_, delta) => {
if (!isHovered) {
setOffsetX((prev) => prev - delta * speed);
}
});
You won’t believe how much better it felt immediately — butter-smooth motion without any lag.
One problem I faced initially was abrupt jumps when resetting scroll. It broke the immersion.
After few late-night experiments, I cracked it. By duplicating the list twice side by side, and translating it by the modulus of total width, I got a clean, continuous loop.
const translateX = offsetX % totalContentWidth;
It felt like magic when I first saw it working properly — no stutter, no jarring jump.
I realized some users might want to interact with a card (like clicking on a trending plugin). So I added hover detection to pause scrolling whenever needed:
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Card Content */}
</div>
Simple touches like this made it feel much more thoughtful and polished.
Putting everything together, here’s a clean reusable InfiniteScroll component:
import { useState } from "react";
import { motion, useAnimationFrame } from "framer-motion";
const InfiniteScroll = ({ items }: { items: string[] }) => {
const [offsetX, setOffsetX] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const speed = 0.05;
const cardWidth = 200;
const gap = 10;
const totalWidth = items.length * (cardWidth + gap);
useAnimationFrame((_, delta) => {
if (!isHovered) {
setOffsetX((prev) => prev - delta * speed);
}
});
const translateX = offsetX % totalWidth;
return (
<div style={{ overflow: "hidden", whiteSpace: "nowrap" }}>
<motion.div
style={{
display: "flex",
gap: `${gap}px`,
transform: `translateX(${translateX}px)`,
}}
>
{[...items, ...items].map((item, index) => (
<div
key={index}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
width: `${cardWidth}px`,
height: "120px",
backgroundColor: "#f0f0f0",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "8px",
boxShadow: "0px 2px 4px rgba(0,0,0,0.1)",
cursor: "pointer",
}}
>
{item}
</div>
))}
</motion.div>
</div>
);
};
export default InfiniteScroll;
After plugging it into ObsidianStats, the difference was immediately visible.
And honestly, it became one of my favourite parts of the new design. A small animation that added a signature feel to the site, without me even planning for it initially.
Sometimes small UI touches make the biggest difference.
By building this lightweight infinite scroll with Framer Motion, I kept the homepage fast, smooth, and visually richer.
If you are working on any feature where you want to subtly highlight items — give this technique a try. You might be surprised at how big an impact such a tiny detail can make!
Thanks for reading, and stay tuned — next time I’ll share more behind-the-scenes stories and practical UI tips!
What would you like me to cover next — marquee text, animated carousels, or maybe entrance animations? Tell me in the comments!