An explanation of the ScrollVelocity component: creating a high-performance scroll-driven marquee using Framer Motion and GPU-accelerated canvas translations.
Technical breakdown of the Scroll Based Velocity component: an interactive marquee that dynamically accelerates, decelerates, and reverses direction in response to page scrolling velocity. Using Framer Motion hooks, React Context synchronization, and viewport observers, the component delivers smooth, hardware-accelerated loops while respecting reduced motion preferences.
The flowchart below visualizes the data pipeline of the scroll-based marquee, representing the journey from browser scroll input to the final GPU-accelerated layout transformation:
To create a marquee that responds organically to user interaction, the component monitors scroll event variables and refines them using a mass-spring physics engine.
When you scroll a web page (especially with a mouse wheel), the movement happens in sudden ticks. If we mapped this raw scroll speed directly to the marquee, it would speed up abruptly and stop instantly, resulting in a laggy and choppy animation.
To fix this, the component passes the raw speed values through a virtual physics spring:
stiffness: 400: Controls how quickly the spring reacts. A higher value makes the marquee accelerate faster the moment you start scrolling.
damping: 50: Acts like physical friction. Instead of the marquee stopping abruptly when you release scroll inputs, it lets the speed slide and decay gradually back to its base rate, like a heavy spinning wheel slowing to a stop.
If a developer places multiple row elements on a single page (e.g., three marquee bands with different sentences or directions), running local listeners on each band creates redundant event bindings:
Active Observers = 3 * (useScroll + useVelocity + useSpring)
To optimize this, the component implements a synchronized context architecture to share state:
Declare Context & Container Provider:
The container instantiates the scroll hooks once, computes the global velocityFactorMotionValue, and broadcasts it using React Context:
Consuming Context with Local Fallback:
The individual rows look up the context. If found, they run synchronously from the shared velocity value. If missing, they fall back to instantiating local scroll observers:
Math.ceil(cw / bw): Computes the minimum number of copies required to fill the screen space.
Example: If cw = 1000px and bw = 300px, then 1000 / 300 = 3.33.
Using Math.floor (rounding down to 3) yields target dimensions of only 900px, leaving a 100px empty space.
Using Math.ceil (rounding up to 4) spans the text across 1200px, fully covering the viewport with room to slide.
+ 2 Buffer Tiles: Provides padding. As the horizontal row moves, the far-left coordinate tile slides out of view. The two buffer tiles ensure a replacement tile is ready to emerge on the right, preventing layout gaps.
Math.max(3, ...): Sets a minimum count of 3 copies to support short strings on wide screens.
Layout dimensions change during window resizing, device rotations, or dynamic font loads. To capture these modifications without leaking memory, the component uses ResizeObserver and IntersectionObserver paired with a React cleanup function:
The marquee creates the illusion of infinite scrolling by looping a closed track of identical items. The reset must occur instantly at coordinates where the copy matches the starting tile's position.
The useAnimationFrame callback provides delta (time elapsed since the last frame in milliseconds). The component normalizes this into seconds:
dt = delta / 1000
To see how this creates consistency, let's look at an example where the marquee's calculated speed is 20px/s (how this speed is calculated is detailed in the next section):
60Hz monitor (renders 60 frames per second): dt ≈ 0.0166s (moves 20px/s * 0.0166s ≈ 0.33px per frame)
120Hz monitor (renders 120 frames per second): dt ≈ 0.0083s (moves 20px/s * 0.0083s ≈ 0.16px per frame)
Over a full second, the text travels the exact same distance (20 pixels) on both monitors. This ensures the content moves at the same speed regardless of display frame rates.
The movement calculation compiles multiple values:
moveBy = direction * speed * speedMultiplier * dt
Pixels per Second Calculation:
speed = (bw * baseVelocity) / 100
If a marquee has a block width (bw) of 400px and a baseVelocity of 5, the base speed is (400 * 5) / 100 = 20px/s. This scaling ensures content behaves consistently relative to its length (longer text moves more pixels per second, maintaining relative pace).
Speed Multiplier:
Is set to 1 (normal speed) at rest. Scaling increases to 1 + v_factor during active scrolling.
Coordinate Update:
Adds moveBy to baseX via .set(current + moveBy). This updates the MotionValue directly, bypassing React state re-renders to maintain rendering performance.
[!NOTE]
Switching browser tabs does not unmount or destroy the React component; the page remains loaded and active in the background. Pausing the loop avoids wasting CPU and battery power when the tab is inactive.
The moving elements are translated using CSS transform. To keep the scrolling butter-smooth, the component combines two styling properties to offload layout work to the GPU and keep it there:
transform-gpu: Forces the browser to translate the element using 3D rendering (via translate3d), moving calculations from the CPU to the GPU.
will-change-transform: Tells the browser to keep that GPU layer active in memory.
Why we need both:
Layer Thrashing Prevention: While transform-gpu pushes the animation to the GPU, browser rendering engines will dynamically destroy GPU layers when elements slow down or stop to save memory. Pairing it with will-change-transform locks the layer in graphics memory, eliminating the framerate stutters that occur when a browser continually recreates active GPU layers.
Guaranteeing Promotion: The will-change property is technically a hint—browsers can choose to ignore it if system memory is low. Pairing it with transform-gpu (3D transforms) forces immediate promotion in the graphics pipeline and acts as a fallback for older devices/browsers that don't support will-change.
The component accommodates user-level system preferences:
Reduced Motion Support:
Some users enable "Reduce Motion" in their operating system settings to avoid motion sickness or sensory issues from rapid UI movements. The component queries this setting and listens to updates live:
By keeping the multiplier at 1, we ignore page scroll velocity spikes (absVf) and keep the marquee moving at its slow, gentle, constant base speed.
Screen Reader Clean Flow:
Tiling text duplicates can cause screen readers to repeat announcements. The component sets aria-hidden={true} on all duplicates except the first: