Scroll Based Velocity

Scrolling text whose speed changes based on scroll speed

Velocity Scroll
Velocity Scroll

Installation

Install the following dependencies:

npm install framer-motion

Copy and paste the following code into your project.

"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 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>
  )
}

Update the import paths to match your project setup.

Props

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