Text Marquee

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

Installation

npm install framer-motion @motionone/utils
text-marquee.tsx
1
'use client';
2
import { useRef, useEffect } from 'react';
3
import {
4
motion,
5
useScroll,
6
useSpring,
7
useTransform,
8
useVelocity,
9
useAnimationFrame,
10
useMotionValue,
11
} from 'framer-motion';
12
import { wrap } from '@motionone/utils';
13
import { cn } from '@/lib/utils';
14
15
interface ParallaxProps {
16
children: string;
17
baseVelocity: number;
18
clasname?: string;
19
scrollDependent?: boolean; // Toggle scroll-dependent behavior
20
delay?: number; // Delay before animation starts
21
}
22
23
export default function ScrollBaseAnimation({
24
children,
25
baseVelocity = -5,
26
clasname,
27
scrollDependent = false, // Default to false
28
delay = 0, // Default delay is 0 (no delay)
29
}: ParallaxProps) {
30
const baseX = useMotionValue(0);
31
const { scrollY } = useScroll();
32
const scrollVelocity = useVelocity(scrollY);
33
const smoothVelocity = useSpring(scrollVelocity, {
34
damping: 50,
35
stiffness: 400,
36
});
37
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 2], {
38
clamp: false,
39
});
40
41
const x = useTransform(baseX, (v) => `${wrap(-20, -45, v)}%`);
42
43
const directionFactor = useRef<number>(1);
44
const hasStarted = useRef(false); // Track animation start status
45
46
useEffect(() => {
47
const timer = setTimeout(() => {
48
hasStarted.current = true; // Start animation after the delay
49
}, delay);
50
51
return () => clearTimeout(timer); // Cleanup on unmount
52
}, [delay]);
53
54
useAnimationFrame((t, delta) => {
55
if (!hasStarted.current) return; // Skip if delay hasn't passed
56
57
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
58
59
// Reverse direction if scrollDependent is true
60
if (scrollDependent) {
61
if (velocityFactor.get() < 0) {
62
directionFactor.current = -1;
63
} else if (velocityFactor.get() > 0) {
64
directionFactor.current = 1;
65
}
66
}
67
68
moveBy += directionFactor.current * moveBy * velocityFactor.get();
69
70
baseX.set(baseX.get() + moveBy);
71
});
72
73
return (
74
<div className='overflow-hidden whitespace-nowrap flex flex-nowrap'>
75
<motion.div
76
className='flex whitespace-nowrap gap-10 flex-nowrap'
77
style={{ x }}
78
>
79
<span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
80
<span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
81
<span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
82
<span className={cn(`block text-[8vw]`, clasname)}>{children}</span>
83
</motion.div>
84
</div>
85
);
86
}

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).