Troubleshooting
Common problems, how to recognise their cause, and how to fix them.
Start with the browser console: vidscroll logs a clear message for almost
every problem below, starting with [vidscroll].
The video scrolls away instead of pinning
Cause: an element around the video has overflow: hidden, auto or
scroll, which stops position: sticky from working.
Check: in the browser’s inspector, walk up from the [data-vidscroll]
element and look for an overflow setting. Layout wrappers and page
transition components often add one.
Fix: remove it, or change it to overflow: clip, which hides overflow
without breaking sticky.
Scrubbing is slow or choppy
Cause: almost always keyframes far apart in the file. The console says “isn’t encoded for scrubbing” or “will scrub slowly”.
Check: look for those messages. If you see “re-encoding it in the browser”, scrubbing gets smoother once the corner badge disappears.
Fix: prepare the file with npx vidscroll encode. If the file is already
prepared and it’s still choppy on a phone, the effects on top may be the
problem: avoid animated filter: blur() and large shadows.
See Why seeking stutters.
The video never loads
Cause: usually a wrong path or a server problem.
Check: the loading overlay shows the error, and so does the console:
| Message mentions | Meaning |
|---|---|
| HTTP 404 (or another status) | The file isn’t at that URL |
| returned an HTML page | The path is wrong and the host served a web page instead |
| a YouTube, Vimeo or other page | src is a video page, not a file |
| can’t scrub | The browser can’t play this file; prepare it with the CLI |
Fix: use a direct link to the video file. Files in public/ are served
from the site root: public/videos/hero.mp4 is /videos/hero.mp4.
It works locally but not on my phone
Cause: testing your dev server over the local network uses plain HTTP, and browsers turn off in-browser re-encoding and caching on insecure pages. Firefox for Android has no re-encoding at all.
Fix: test on an HTTPS deployment, and prepare videos with the CLI so they don’t depend on re-encoding. See Measuring on real devices.
The video re-encodes on every visit
Cause: the optimized copy isn’t being cached.
Check: the server must send an ETag, Last-Modified or Content-Length
header (curl -sI <url> shows them), the page must be HTTPS or localhost,
and optimize must not set cache: false.
Fix: prepare the file with the CLI, so there’s nothing to re-encode.
A video from another site scrubs slowly
Cause: the server doesn’t allow your site to read the file (CORS), so vidscroll streams it without checking or optimizing it. The console says “Couldn’t read … (network error or CORS)”.
Fix: allow your origin in the server’s CORS settings, or upload a file prepared with the CLI. See Videos from other domains.
Content below the video jumps
Cause: length is "auto", so the block’s height depends on the video’s
duration, which is only known once it loads.
Fix: set length explicitly, such as length="400vh".
Sections appear at the wrong time, or never
| Situation | Cause | Fix |
|---|---|---|
| A time-based section shows up late | Time ranges wait until the duration is known | Expected; it appears as soon as the video’s metadata loads |
| Sections never activate in Astro | Sections written in an .astro file are static HTML | Put ScrollVideo and its sections in one React component (Astro) |
Time ranges do nothing in ScrollFrames | Image sequences have no duration | Use start/end or frame numbers |
A section shifts when you change easing | It uses start/end, which are scroll positions | Use fromTime/toTime to tie it to the footage |
| “Section id already exists” | Two sections share an id | Make ids unique within the video |
Buttons over the video don’t respond
Cause: the layer over the video ignores clicks so you can scroll through it.
Fix: give interactive elements pointer-events: auto. Inside a section,
the section’s content already takes clicks while it’s active.
Hooks return null or never update
Cause: the component isn’t inside a ScrollVideo (or ScrollFrames).
Outside one, useScrollVideo returns { api: null } and the others stay idle.
Fix: render the component as a child of the ScrollVideo.
Next.js: “Functions cannot be passed directly to Client Components”
Cause: a function prop, such as a custom easing, onLoad or loader,
passed from a server component.
Fix: use a preset easing name, or move the scroll video into your own
component that starts with "use client". See Next.js.
Several raw videos on one page in Firefox
Cause: Firefox may refuse to run more than one video encoder at a time, so only one video gets re-encoded; the console says “Re-encoding … failed”.
Fix: prepare the videos with the CLI.