@vectorvesper/motion/reactCore Media · Timeline Scrubbing

useVideoScrubber

Assigning HTMLMediaElement.currentTime directly during scroll or pointer events causes seek collisions, frame drops, and browser decoding stalls. useVideoScrubber provides seek-disciplined, frame-rate independent video timeline scrubbing across scroll, pointer, and manual drivers.

The useVideoScrubber hook binds video playback progress to external physical inputs. It provides robust seek discipline: new seeks are never issued while previous seeks are in flight, sub-frame target deltas are discarded as noise, and jumps exceeding 350ms leverage hardware fastSeek() where supported.

To resolve iOS Safari buffering constraints, the controller automatically executes a silent play/pause handshake on mount, forcing the browser to load initial video chunks.

The hook supports three operational drivers: "scroll" (with sticky pinning or viewport crossing), "pointer" (horizontal or vertical drag/hover), and "manual" (imperative timeline control via controller methods).

Pointer-Driven Film Strip Scrubbing

SEEK-DISCIPLINED PLAYBACK
Demo loads on scroll

Quick start

Bind a sticky video container to scroll progress. Set mapping to 'pin' for scrollytelling sections.

Always encode scrubbing videos with keyframes on every frame (g=1) to prevent decoding lag.

Scrub Asset Encoding & Scroll Mapping

Standard delivery videos with long GOP intervals stutter when scrubbed backwards. Re-encode assets with I-frames only for instant seeks.

Keyframe Encoding Command (FFmpeg)

Standard MP4 files store keyframes every ~250 frames; seeking backwards requires decoding all intermediate P-frames. Use this FFmpeg preset to generate all-intra (I-frame only) video clips:

ffmpeg -i input.mp4 -g 1 -coder 0 -bf 0 -crf 20 -movflags +faststart scrub.mp4
Scroll Mapping Options
MappingProgress 0.0Progress 1.0Ideal Pattern
"pin"Track top docks at viewport topTrack bottom reaches viewport bottomTall sticky scrollytelling scenes
"cross"Track top enters viewport bottomTrack bottom exits viewport topIn-flow scrolling reveal sections
"auto"Uses "pin" if track height > 1.2 viewports, else "cross"Automatic default

API Reference

Syntax

Synchronizes HTMLVideoElement playback time with scroll offsets, pointer coordinates, or manual values.

Parameters

Parameters.fieldTypeDefaultDescription
options.driver"scroll" | "pointer" | "manual""scroll"Source driving video progress. Fixed for instance lifecycle.
options.mapping"auto" | "pin" | "cross""auto"Scroll to progress calculation method (scroll driver only).
options.speednumber8Catch-up damping rate. 0 is instant tracking; higher values follow closer.
options.pointerAxis"x" | "y""x"Axis on track bounding box mapped to timeline (pointer driver only).
options.onProgress(progress: number, time: number) => voidundefinedCallback fired when smoothed progress changes, deduplicated per frame.

Return Value

UseVideoScrubberReturn<TTrack>

Contains video element ref, track ref, non-reactive progressRef, and controller scrubberRef.

Properties & State

Properties.fieldTypeDescription
videoRefRefObject<HTMLVideoElement | null>Ref attached to the target video element.
refRefObject<TTrack | null>Ref attached to the container defining scroll or pointer geometry.
progressRefRefObject<number>0.0 to 1.0 smoothed progress. Updated per frame with zero component re-renders.
scrubberRefRefObject<VideoScrubber | null>Direct reference to core VideoScrubber instance for imperative methods.

Driver Comparison

Driver Comparison.fieldSource InputPrimary Use Case
"scroll"Track bounding rect vs viewport offsetSticky hero stories and scrollytelling articles.
"pointer"Normalized pointer coordinates across trackInteractive film strip scrubber and video thumbnails.
"manual"Explicit programmatic calls to scrubber.set(p)Custom sliders, audio waveform sync, or external timelines.

Video Attribute Restoration & Teardown

VideoScrubber records initial video element attributes on mount and restores them on unmount.

1. React Hook UsageAutomatic Teardown
2. Vanilla JS UsageManual Instance Destroy

Lifecycle & Invariant Guarantees

  • Zero seek collisions: Waits for active seeks to complete before issuing subsequent seek requests, preventing Safari video stalls.
  • iOS Safari auto-priming: Performs an automatic silent play/pause handshake on mount to prime hardware decoding buffers.
  • Hardware fastSeek integration: Switches to fastSeek() when jumping more than 350ms across timeline, providing keyframe-fast scrubbing.
  • Sub-frame noise filtering: Discards seek deltas smaller than 1/60th second, eliminating redundant decoder pressure.

Production Examples

Battle-tested production patterns ready to copy directly into your codebase.

Tall pinned scroll track where scroll distance controls video timeline playback.

When NOT to use this

Standard unconstrained video playback without interaction.
→ instead Use standard HTML5 <video autoplay muted playsinline>. Scrubbing machinery is only needed when binding to user inputs.
Using delivery-encoded long-GOP video files without frequent keyframes.
→ instead Re-encode assets with FFmpeg (-g 1) before implementing scrubbing; no algorithm can eliminate long-GOP decoding latency.
Animating simple geometric reveals or logo draw-on effects.
→ instead Use SVG stroke-dashoffset or canvas paths; video files consume significant memory and decoder threads.

Rules & Gotchas

  • Encode dedicated scrubbing video assets: Ensure source files have keyframes on every frame (g=1). The controller eliminates seek collisions but cannot speed up long-GOP decodes.
  • Provide sufficient track height for scroll drivers: Track height acts as the pacing lever: taller tracks yield smoother, more deliberate scrubbing feel.
  • Read progressRef inside frame subscriptions: Read progress directly from progressRef inside render callbacks to avoid 60fps React state updates.
  • Preserve muted and playsInline attributes: The controller manages scrub attributes on mount and restores them on unmount; do not remove muted attributes on mobile.

Related Motion Components