Introducing Magic UI Pro - 50+ blocks and templates to build beautiful landing pages in minutes.


Docs
Scroll Based Velocity

Scroll Based Velocity

Scrolling text whose speed changes based on scroll speed

Velocity Scroll
Velocity Scroll

Installation

pnpm i @motionone/utils

Copy and paste the following code into your project.

components/magicui/scroll-based-velocity.tsx
"use client";
 
import { cn } from "@/lib/utils";
import { wrap } from "@motionone/utils";
import {
  motion,
  useAnimationFrame,
  useMotionValue,
  useScroll,
  useSpring,
  useTransform,
  useVelocity,
} from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
 
interface VelocityScrollProps {
  text: string;
  default_velocity?: number;
  className?: string;
}
 
interface ParallaxProps {
  children: string;
  baseVelocity: number;
  className?: string;
}
 
export function VelocityScroll({
  text,
  default_velocity = 5,
  className,
}: VelocityScrollProps) {
  function ParallaxText({
    children,
    baseVelocity = 100,
    className,
  }: ParallaxProps) {
    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 = React.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>
  );
}

Props

PropTypeDescriptionDefault
classNamestringThe class name to be applied to the component
textstringText to be animated""
default_velocitynumberBase scroll velocity of text5