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
Prop | Type | Description | Default |
---|---|---|---|
className | string | The class name to be applied to the component | |
text | string | Text to be animated | "" |
default_velocity | number | Base scroll velocity of text | 5 |