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
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.
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| Mapping | Progress 0.0 | Progress 1.0 | Ideal Pattern |
|---|---|---|---|
| "pin" | Track top docks at viewport top | Track bottom reaches viewport bottom | Tall sticky scrollytelling scenes |
| "cross" | Track top enters viewport bottom | Track bottom exits viewport top | In-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.field | Type | Default | Description |
|---|---|---|---|
| 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.speed | number | 8 | Catch-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) => void | undefined | Callback fired when smoothed progress changes, deduplicated per frame. |
Return Value
Contains video element ref, track ref, non-reactive progressRef, and controller scrubberRef.
Properties & State
| Properties.field | Type | Description |
|---|---|---|
| videoRef | RefObject<HTMLVideoElement | null> | Ref attached to the target video element. |
| ref | RefObject<TTrack | null> | Ref attached to the container defining scroll or pointer geometry. |
| progressRef | RefObject<number> | 0.0 to 1.0 smoothed progress. Updated per frame with zero component re-renders. |
| scrubberRef | RefObject<VideoScrubber | null> | Direct reference to core VideoScrubber instance for imperative methods. |
Driver Comparison
| Driver Comparison.field | Source Input | Primary Use Case |
|---|---|---|
| "scroll" | Track bounding rect vs viewport offset | Sticky hero stories and scrollytelling articles. |
| "pointer" | Normalized pointer coordinates across track | Interactive 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.
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
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.