A momentum bars component that animates a set of bars to follow the mouse cursor.
<MomentumBars
barCount={16}
pullFactor={0.6}
maxShift={40}
opacityRange={200}
minOpacity={0.2}
maxOpacity={1}
minScale={0.95}
maxScale={1.20}
className="h-[50px] gap-4"
/>Install the following dependencies:
npm install motionbun add motiondeno add motionpnpm add motionyarn add motionCopy and paste the following code into your project.
import { motion, type MotionValue, useMotionValue, useSpring, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
const DEFAULT_BAR_COUNT = 16;
const DEFAULT_PULL_FACTOR = 0.6;
const DEFAULT_MAX_SHIFT = 40;
const DEFAULT_OPACITY_RANGE = 200;
const DEFAULT_MIN_OPACITY = 0.2;
const DEFAULT_MAX_OPACITY = 1;
const DEFAULT_MIN_SCALE = 0.95;
const DEFAULT_MAX_SCALE = 1.2;
const MOMENTUM_BARS_DEFAULTS = {
barCount: DEFAULT_BAR_COUNT,
pullFactor: DEFAULT_PULL_FACTOR,
maxShift: DEFAULT_MAX_SHIFT,
opacityRange: DEFAULT_OPACITY_RANGE,
minOpacity: DEFAULT_MIN_OPACITY,
maxOpacity: DEFAULT_MAX_OPACITY,
minScale: DEFAULT_MIN_SCALE,
maxScale: DEFAULT_MAX_SCALE,
} as const;
type MomentumBarsProps = {
barCount?: number;
pullFactor?: number;
maxShift?: number;
opacityRange?: number;
minOpacity?: number;
maxOpacity?: number;
minScale?: number;
maxScale?: number;
className?: string;
}
export { MOMENTUM_BARS_DEFAULTS, type MomentumBarsProps };
export default function MomentumBars({
barCount = DEFAULT_BAR_COUNT,
pullFactor = DEFAULT_PULL_FACTOR,
maxShift = DEFAULT_MAX_SHIFT,
opacityRange = DEFAULT_OPACITY_RANGE,
minOpacity = DEFAULT_MIN_OPACITY,
maxOpacity = DEFAULT_MAX_OPACITY,
minScale = DEFAULT_MIN_SCALE,
maxScale = DEFAULT_MAX_SCALE,
className,
}: MomentumBarsProps) {
const containerRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(-1);
const [containerWidth, setContainerWidth] = useState(0);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const update = () => setContainerWidth(el.getBoundingClientRect().width);
update();
const observer = new ResizeObserver(update);
observer.observe(el);
return () => observer.disconnect();
}, []);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
mouseX.set(e.clientX - rect.left);
};
const handleMouseLeave = () => {
mouseX.set(-1);
};
return (
<div
role="region"
aria-hidden="true"
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn("gap-8 flex items-center justify-center h-[200px]", className)}
>
{[...Array(barCount)].map((_, i) => (
<Bar
key={i}
index={i}
mouseX={mouseX}
containerWidth={containerWidth}
barCount={barCount}
pullFactor={pullFactor}
opacityRange={opacityRange}
minOpacity={minOpacity}
maxOpacity={maxOpacity}
minScale={minScale}
maxScale={maxScale}
maxShift={maxShift}
/>
))}
</div>
);
}
function Bar({
index,
mouseX,
containerWidth,
barCount,
pullFactor,
opacityRange,
minOpacity,
maxOpacity,
minScale,
maxScale,
maxShift,
}: {
index: number;
mouseX: MotionValue<number>;
containerWidth: number;
barCount: number;
pullFactor: number;
opacityRange: number;
minOpacity: number;
maxOpacity: number;
minScale: number;
maxScale: number;
maxShift: number;
}) {
const homeX = (index / barCount) * containerWidth;
const x = useTransform(mouseX, (latest) => {
if (latest < 0) return 0;
const delta = latest - homeX;
const shift = delta * pullFactor;
return Math.max(-maxShift, Math.min(maxShift, shift));
});
const opacity = useTransform(mouseX, (latest) => {
if (latest < 0) return minOpacity;
const distance = Math.abs(latest - homeX);
const t = Math.min(1, distance / opacityRange);
return maxOpacity - (maxOpacity - minOpacity) * t;
});
const scale = useTransform(mouseX, (latest) => {
if (latest < 0) return 1;
const distance = Math.abs(latest - homeX);
const t = Math.min(1, distance / opacityRange);
return maxScale - (maxScale - minScale) * t;
});
const springX = useSpring(x, { damping: 35, stiffness: 200, bounce: 1, mass: 1.5 });
const springOpacity = useSpring(opacity, { damping: 35, stiffness: 200, bounce: 1, mass: 1.5 });
const springScale = useSpring(scale, { damping: 35, stiffness: 200, bounce: 1, mass: 1.5 });
return (
<motion.div
style={{
translateX: springX,
opacity: springOpacity,
height: "100%",
scale: springScale,
}}
className="bg-white h-full w-[2px] rounded-1"
/>
);
}It accepts the following props:
| Prop | Type | Default Value | Description |
|---|---|---|---|
| barCount | number | 16 | The number of bars to display |
| pullFactor | number | 0.6 | The factor by which the bars pull towards the mouse cursor |
| maxShift | number | 40 | The maximum shift of the bars from their home position |
| opacityRange | number | 200 | The range of opacity values for the bars |
| minOpacity | number | 0.2 | The minimum opacity value for the bars |
| maxOpacity | number | 1 | The maximum opacity value for the bars |
| minScale | number | 0.95 | The minimum scale value for the bars |
| maxScale | number | 1.20 | The maximum scale value for the bars |
| className | string | - | The class name to be applied to the component |