Leave a star on GitHub! ⭐


Docs
Scroll Based Velocity

Scroll Based Velocity

Text animation that changes speed based on scroll position.

Eldora UI
Eldora UI

Installation

pnpm add clsx framer-motion

Copy and paste the following code into your project.

components/eldoraui/scrollbasedvelocity.tsx

"use client";
"use client";
 
import React, { useEffect, useRef, useState } from "react";
import {
  motion,
  useAnimationFrame,
  useMotionValue,
  useScroll,
  useSpring,
  useTransform,
  useVelocity,
} from "framer-motion";
 
import { cn } from "@/lib/utils";
 
interface VelocityScrollProps {
  text: string;
  default_velocity?: number;
  className?: string;
}
 
interface ParallaxProps {
  children: string;
  baseVelocity: number;
  className?: string;
}
 
export const wrap = (min: number, max: number, v: number) => {
  const rangeSize = max - min;
  return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
 
export const VelocityScroll: React.FC<VelocityScrollProps> = ({
  text,
  default_velocity = 5,
  className,
}) => {
  const ParallaxText: React.FC<ParallaxProps> = ({
    children,
    baseVelocity = 100,
    className,
  }) => {
    const baseX = useMotionValue(0);
    const { scrollY } = useScroll();
    const scrollVelocity = useVelocity(scrollY);
    const smoothVelocity = useSpring(scrollVelocity, {
      damping: 50,
      stiffness: 400,
    });
 
    const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
      clamp: false,
    });
 
    const [repetitions, setRepetitions] = useState(1);
    const containerRef = useRef<HTMLDivElement>(null);
    const textRef = useRef<HTMLSpanElement>(null);
 
    useEffect(() => {
      const calculateRepetitions = () => {
        if (containerRef.current && textRef.current) {
          const containerWidth = containerRef.current.offsetWidth;
          const textWidth = textRef.current.offsetWidth;
          const newRepetitions = Math.ceil(containerWidth / textWidth) + 2;
          setRepetitions(newRepetitions);
        }
      };
 
      calculateRepetitions();
 
      window.addEventListener("resize", calculateRepetitions);
      return () => window.removeEventListener("resize", calculateRepetitions);
    }, [children]);
 
    const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`);
 
    const directionFactor = useRef<number>(1);
    useAnimationFrame((t, delta) => {
      let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
 
      if (velocityFactor.get() < 0) {
        directionFactor.current = -1;
      } else if (velocityFactor.get() > 0) {
        directionFactor.current = 1;
      }
 
      moveBy += directionFactor.current * moveBy * velocityFactor.get();
 
      baseX.set(baseX.get() + moveBy);
    });
 
    return (
      <div
        className="w-full overflow-hidden whitespace-nowrap"
        ref={containerRef}
      >
        <motion.div className={cn("inline-block", className)} style={{ x }}>
          {Array.from({ length: repetitions }).map((_, i) => (
            <span key={i} ref={i === 0 ? textRef : null}>
              {children}{" "}
            </span>
          ))}
        </motion.div>
      </div>
    );
  };
 
  return (
    <section className="relative w-full">
      <ParallaxText baseVelocity={default_velocity} className={className}>
        {text}
      </ParallaxText>
      <ParallaxText baseVelocity={-default_velocity} className={className}>
        {text}
      </ParallaxText>
    </section>
  );
};

Update the import paths to match your project setup.

Props

Prop NameTypeDefaultDescription
textstring-The text to scroll horizontally with velocity effect.
default_velocitynumber5The default velocity for the scrolling effect.
classNamestring""Additional CSS classes for custom styling.