Scroll-Linked Video Scrubber for React

Sticky video that scrubs forward/backward as you scroll. Uses a tall wrapper and position: sticky - no scroll hijacking.

Component

Installation

 

Usage

Import

Add the imports.

import { useRef } from "react";
import { ScrollLinkedVideoScrubber } from "@/components/scroll-linked-video-scrubber";

Use

Wrap in a scrollable container. Pass scroller ref. scrollHeight controls scrub speed.

const containerRef = useRef<HTMLDivElement>(null);

<div
  ref={containerRef}
  className="h-screen overflow-y-auto"
>
  <ScrollLinkedVideoScrubber
    src="/components/scroll-linked-video-scrubber/demo.mp4"
    scroller={containerRef}
    scrollHeight="500%"
  />
</div>;

Guidelines

  • Pass src for the video URL. Video must be same-origin or CORS-enabled for duration access.
  • When inside a scrollable container (demo, modal), pass scroller={containerRef}.
  • scrollHeight controls how much scroll is needed to play the full video (default '300%').
  • No extra spacer divs needed - the component creates its own scroll runway.

Props

All props are optional unless marked required. Use these to customize every aspect of the component.

PropTypeDefaultDescription
srcrequiredstring-Video source URL.
classNamestring-Additional CSS classes for the outer wrapper.
videoClassNamestring-Classes for the video element.
scrollerReact.RefObject<HTMLElement | null>-Ref to the scroll container. When inside overflow-y-auto, pass its ref so scroll events track that element.
posterstring-Poster image URL.
mutedbooleantrueMute the video (recommended for autoplay policies).
playsInlinebooleantrueplaysInline for mobile.
scrollHeightstring"300%"Height of the scroll runway. Larger values = slower scrub. Accepts any CSS height value.

Accessibility

  • Decorative scroll-driven media effect with no interactive controls; the video element has no controls attribute, so there are no focusable play, pause, or seek buttons to operate by keyboard.
  • The video exposes no ARIA role, label, or text alternative and ships no captions or track element, so non-visual users get no equivalent for the scrubbed footage; add an accessible name and captions if the video conveys meaningful information.
  • Playback is driven entirely by scroll position, which is keyboard reachable via normal scrolling, but there is no independent control to pause or scrub the media on its own.
  • It does not check prefers-reduced-motion, so the scroll-linked motion plays for everyone; it should be guarded with a matchMedia('(prefers-reduced-motion: reduce)') query to hold a static frame or poster for users who prefer reduced motion.

Performance

  • Each scroll tick sets video.currentTime to map progress onto the timeline, which triggers per-frame video decode and seeking; this is heavier than animating transform or opacity and can stutter on long or high-resolution clips.
  • The scroll listener is registered with passive true so it never blocks the scroll thread, and pinning is done with CSS position sticky rather than JS-driven top offsets, keeping layout cheap.
  • It reads getBoundingClientRect inside the scroll handler on every event without requestAnimationFrame throttling or IntersectionObserver gating, so updates fire even when the wrapper is offscreen and can cause layout reads on every scroll frame.
  • A ResizeObserver tracks the scroll container height to size the sticky region, and both the scroll and loadedmetadata listeners plus the observer are disconnected in their effect cleanups, so no listeners or observers leak.
  • There is no visibility or reduced-motion gating, so decoding and seeking continue regardless of whether the video is in view or the user prefers reduced motion.

Examples

Basic

Basic usage. Pass scroller ref when inside a scrollable container.

import { useRef } from "react";
import { ScrollLinkedVideoScrubber } from "@/components/scroll-linked-video-scrubber";

export function VideoScrubberDemo() {
  const containerRef = useRef<HTMLDivElement>(null);
  return (
    <div
      ref={containerRef}
      className="h-96 overflow-y-auto"
    >
      <ScrollLinkedVideoScrubber
        src="/components/scroll-linked-video-scrubber/demo.mp4"
        scroller={containerRef}
        scrollHeight="500%"
      />
    </div>
  );
}

Last updated on Jun 19

Made with ❤️ by Pulkit &

© 2026 Pulkit. All rights reserved

Last updated: