Circle Menu
Dialog Form
Dominoes List Scroll
Dominoes Scroll Indicator
Eagle Vision
Electric AI Input
File Input
Flip Scroll
Glowing Scroll Indicator
Horizontal Scroll
Icon Wheel
Image Pile
Interactive CTA
Interactive Folder
Interest Picker
Jelly Loader
Leave Rating
Mask Cursor Effect
Magnet Tabs
Masonry Grid
OTP Input
Photo Gallery
Pixelated Carousel
Rolling Ball Scroll Indicator
Rubik Cube
Sidebar
Sine Wave
Skeumorphic Music Card
Social Media Card
Stacked Input Form
Stack Scroll
Trading Card
Rolling Ball Scroll Indicator
A scroll indicator that displays animated bars with a rolling ball that moves across them as you scroll. The bars grow and shrink based on your scroll position while a colorful ball rolls smoothly from left to right.
Install dependencies
npm i framer-motion
Rolling Ball Scroll Indicator
Create a file rolling-ball-scroll-indicator.tsx in your components folder and paste this code
'use client';
import { motion, MotionValue, useScroll, useSpring, useTransform } from 'framer-motion';
import { usePathname } from 'next/navigation';
import React, { useEffect } from 'react';
const BARS = 40;
const ScrollBar = ({
index,
scrollProgress
}: {
index: number;
scrollProgress: MotionValue<number>;
}) => {
const thisBarPosition = index / BARS;
// Transform scroll progress directly into distance
const distance = useTransform(scrollProgress, (value) => {
return Math.abs(value - thisBarPosition);
});
// Apply spring to the distance for smooth animation
const springDistance = useSpring(distance, {
stiffness: 700,
damping: 40
});
// Transform the spring distance into height
const height = useTransform(springDistance, [0, 1], [0, 35]);
const opacity = useTransform(springDistance, [0, 0.05, 1], [0, 0.2, 1]);
return (
<motion.div
className="w-0.5 bg-white"
style={{
height: height,
opacity: useTransform(opacity, (value) => `${value}`)
}}
/>
);
};
const ScrollIndicatorBars = ({
container,
direction
}: {
container: HTMLElement;
direction: 'vertical' | 'horizontal';
}) => {
const ref = React.useRef<HTMLElement>(container);
// Update ref if container changes
React.useEffect(() => {
ref.current = container;
}, [container]);
const { scrollXProgress, scrollYProgress } = useScroll({
container: ref
});
const springDistance = useSpring(direction === 'vertical' ? scrollYProgress : scrollXProgress, {
stiffness: 700,
damping: 40
});
const left = useTransform(springDistance, [0, 1], [0, 100]);
const rotateZ = useTransform(springDistance, [0, 1], [0, 3000]);
return (
<div className="flex items-end justify-center gap-1 md:gap-2 relative w-fit">
{Array.from({ length: BARS }).map((_, index) => {
return (
<ScrollBar
key={`scroll-bar-${index}`}
index={index}
scrollProgress={direction === 'vertical' ? scrollYProgress : scrollXProgress}
/>
);
})}
<motion.div
className="h-3.5 w-1 rounded-full absolute bottom-0"
style={{ left: useTransform(left, (value) => `${value}%`) }}
>
<motion.div
style={{ rotateZ: useTransform(rotateZ, (value) => `${value}deg`) }}
className="w-3.5 h-3.5 rounded-full bg-gradient-to-b from-yellow-500 to-green-500 from-50% to-50% absolute top-0 left-1/2 -translate-x-1/2"
></motion.div>
</motion.div>
</div>
);
};
const ScrollIndicator = ({
scrollContainerId,
direction
}: {
scrollContainerId: string;
direction: 'vertical' | 'horizontal';
}) => {
const [container, setContainer] = React.useState<HTMLElement | null>(null);
useEffect(() => {
const scrollContainer = document.getElementById(scrollContainerId);
if (scrollContainer) {
setContainer(scrollContainer);
}
}, [scrollContainerId]);
if (!container) {
return null;
}
return <ScrollIndicatorBars container={container} direction={direction} />;
};
const RollingBallScrollIndicator = ({
scrollContainerId = 'scroll-target',
direction = 'vertical'
}: {
scrollContainerId?: string;
direction?: 'vertical' | 'horizontal';
}) => {
const pathname = usePathname();
return (
<ScrollIndicator key={pathname} scrollContainerId={scrollContainerId} direction={direction} />
);
};
export default RollingBallScrollIndicator;
Usage
<div className="h-full w-full flex items-start justify-center gap-2 relative">
<div
id="scroll-target"
className="h-full w-full overflow-y-scroll absolute top-0 left-0"
>
<div className="flex flex-col items-center justify-start gap-2 pb-14">
{images.map((image, i) => {
return (
<Image key={i} src={image.image} alt={image.image} width={512} height={512} />
);
})}
</div>
</div>
<div className="sticky top-full right-10 z-[999] w-full bg-black/50 p-2 flex items-end justify-center backdrop-blur-xl h-14">
<RollingBallScrollIndicator scrollContainerId="scroll-target" direction="vertical" />
</div>
</div>