Text Marquee

A text marquee animation with options for unidirectional and bidirectional scrolling, which changes direction based on the scroll position

UniDirectional

Star the repo if you like itStar the repo if you like itStar the repo if you like itStar the repo if you like it
Share it if you like itShare it if you like itShare it if you like itShare it if you like it

Bidirectional

Best Component library For DeveloperBest Component library For DeveloperBest Component library For DeveloperBest Component library For Developer

Installation

npm install framer-motion @motionone/utils
text-marquee.tsx
'use client'
import { useRef, useEffect } from 'react'
import {
  motion,
  useScroll,
  useSpring,
  useTransform,
  useVelocity,
  useAnimationFrame,
  useMotionValue,
} from 'framer-motion'
import { wrap } from '@motionone/utils'
import { cn } from '@/lib/utils'
 
interface ParallaxProps {
  children: string
  baseVelocity: number
  clasname?: string
  scrollDependent?: boolean // Toggle scroll-dependent behavior
  delay?: number // Delay before animation starts
}
 
export default function ScrollBaseAnimation({
  children,
  baseVelocity = -5,
  clasname,
  scrollDependent = false, // Default to false
  delay = 0, // Default delay is 0 (no delay)
}: 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, 2], {
    clamp: false,
  })
 
  const x = useTransform(baseX, (v) => `${wrap(-20, -45, v)}%`)
 
  const directionFactor = useRef<number>(1)
  const hasStarted = useRef(false) // Track animation start status
 
  useEffect(() => {
    const timer = setTimeout(() => {
      hasStarted.current = true // Start animation after the delay
    }, delay)
 
    return () => clearTimeout(timer) // Cleanup on unmount
  }, [delay])
 
  useAnimationFrame((t, delta) => {
    if (!hasStarted.current) return // Skip if delay hasn't passed
 
    let moveBy = directionFactor.current * baseVelocity * (delta / 1000)
 
    // Reverse direction if scrollDependent is true
    if (scrollDependent) {
      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="overflow-hidden whitespace-nowrap flex flex-nowrap">
      <motion.div
        className="flex whitespace-nowrap gap-10 flex-nowrap"
        style={{ x }}
      >
        <span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
        <span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
        <span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
        <span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
      </motion.div>
    </div>
  )
}

Props

PropTypeDefaultDescription
childrenstringThe content to be animated with a parallax effect.
baseVelocitynumber-5The base velocity for the parallax effect.
clasnamestringOptional CSS class for styling the component.
scrollDependentbooleanfalseWhether the animation should depend on scroll.
delaynumber0Delay before the animation starts (in milliseconds).