<vid-scroll>
The full reference for the custom element, for pages without React.
<vid-scroll> runs the same engine as the React ScrollVideo: loading,
re-encoding, caching and scrubbing all behave the same. This page lists
everything it supports. To add it to a page, start with
Plain HTML.
How it’s built
The element keeps its own parts (the pinned stage, the video and the loading overlay) inside a shadow root. Your content stays ordinary page content: the element shows its children over the video through a slot. So your CSS styles your content as usual, and frameworks that render the children keep full control of them.
Attributes
| Attribute | Values | Meaning |
|---|---|---|
src | URL | Video file. Without it the element does nothing; removing it stops the video. |
length | “400vh”, “2000px”, “auto” | Scrolling that covers the whole video. |
easing | preset name | Curve from scroll to video time, such as inOutSine. |
fit | cover, contain | How the video fills the screen. Default cover. |
poster | URL, or none | Image while loading. Without it, the video’s first frame is shown. |
smooth-scroll | present, or “false” | Eased mouse-wheel scrolling for the page. |
optimize | “false”, “wait” | Turn off the in-browser re-encode, or keep the loader until it finishes. |
full-preload | “false” | Stream the file directly instead of downloading it first. |
fps | number | Frame rate for frame-based section ranges. |
warmup | “false”, or a number of steps | The pass over the timeline while loading. |
loader | none | Hide the loading overlay and badge. |
debug | present | Log engine internals to the console. |
Changing an attribute takes effect straight away, the same way a prop change
does in React: easing applies immediately, a new length or fps rebuilds
the scroll mapping, and a new src reloads the video.
Options that aren’t attributes
Callbacks and object options, such as optimize settings, go in the options
property. They override the attributes:
const video = document.querySelector("vid-scroll");
video.options = {
optimize: { maxResolution: 1080 },
onLoad: ({ source }) => console.log("served as", source),
};Assign a new object to change them; changing a property of the current object has no effect.
Sections
<vid-scroll-section> shows its content over part of the video. It takes the
same ranges as the React Section:
| Attribute | Meaning |
|---|---|
start, end | Scroll progress, 0 to 1 |
from-time, to-time | Seconds of video |
from-frame, to-frame | Frame numbers |
id | Name used in events. Generated when missing. |
A section must be inside a <vid-scroll>, at any depth. While active it has a
data-active attribute, and it always has a --progress variable from 0 to 1
across its range. Range rules are explained on the Section
page; they’re identical here.
From JavaScript
<script type="module" src="https://cdn.jsdelivr.net/npm/vidscroll@0.3/dist/cdn/vidscroll-element.js"></script>
<vid-scroll id="film" src="/train.mp4" length="400vh" easing="inOutSine">
<nav class="chapters">
<button data-time="1">Doors</button>
<button data-time="13">Open water</button>
<button data-time="25">Away</button>
</nav>
<p class="timecode">0.0 s</p>
</vid-scroll>
<script type="module">
const film = document.getElementById("film");
const timecode = film.querySelector(".timecode");
film.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", () => film.scrollToTime(Number(button.dataset.time)));
});
customElements.whenDefined("vid-scroll").then(() => {
film.api.on("update", ({ time }) => {
timecode.textContent = `${time.toFixed(1)} s`;
});
});
</script>
<style>
.chapters {
position: absolute;
top: 16px;
left: 16px;
display: flex;
gap: 8px;
pointer-events: auto;
}
.timecode {
position: absolute;
bottom: 16px;
left: 16px;
margin: 0;
color: #fff;
font: 500 28px monospace;
}
</style>| Member | What it does |
|---|---|
scrollToTime(seconds, options?) | Scrolls the page to that point in the video, smoothly by default. |
scrollToProgress(p, options?) | Scrolls to a progress from 0 to 1. |
api | The full controller, or null before the element has started. See vidscroll/core. |
options | Object options and callbacks, above. |
The element starts as soon as it’s connected to the page with a src. Scripts
that run before the element is defined should wait for it, as the example does
with customElements.whenDefined("vid-scroll").
Events
All events bubble, so you can listen on the element or any ancestor.
| Event | Fired on | detail |
|---|---|---|
vidscroll-load | element | { source, probe }: how the video was served |
vidscroll-ready | element | The current state. Fires again if the scroll mapping is rebuilt. |
vidscroll-loader | element | The loading state, or null when loading is done |
vidscroll-error | element | The Error |
vidscroll-enter | section | { id } |
vidscroll-exit | section | { id } |
For per-frame updates, use video.api.on("update", fn) rather than an event.
Styling
Your content is styled with ordinary CSS. The element’s own parts are styled through these hooks:
| Hook | What it targets |
|---|---|
vid-scroll::part(stage) | The pinned, screen-sized stage (black background by default) |
vid-scroll::part(video) | The video |
vid-scroll::part(overlay) | The layer holding your content |
vid-scroll::part(loader) | The loading overlay and the corner badge |
vid-scroll[data-phase] | While loading: download, optimize, preparing or error |
vid-scroll[data-background] | While the corner badge shows during a re-encode |
–loader-progress | Loading progress from 0 to 1, on the element |
–video-progress | Scroll progress through the whole video, on the element |
vid-scroll::part(stage) {
background: #111;
}
vid-scroll[data-background]::part(loader) {
display: none;
}A custom loader
A child with slot="loader" replaces the built-in loader while the video
can’t be scrolled yet. It’s hidden during the corner-badge phase, and errors
always show the built-in message. Use --loader-progress to show progress:
<vid-scroll src="/videos/hero.mp4" length="400vh">
<div slot="loader" class="my-loader">
<div class="my-loader__bar"></div>
</div>
</vid-scroll>
<style>
.my-loader__bar {
width: calc(var(--loader-progress, 0) * 100%);
height: 2px;
background: white;
}
</style>In a page
- The element is a block: its height is
lengthplus one screen, set when it starts. Setlengthexplicitly when content follows it. - A parent with
overflow: hidden,autoorscrollstops the video from pinning. Useoverflow: clipinstead. - Removing the element from the page stops it; adding it back starts it again.
TypeScript
Importing vidscroll/element registers the element types, so
document.querySelector("vid-scroll") is typed as VidScrollElement. Event
detail types are exported as VidScrollEventMap:
import "vidscroll/element";
import type { VidScrollEventMap } from "vidscroll/element";
const video = document.querySelector("vid-scroll")!;
video.addEventListener("vidscroll-loader", (event) => {
const { detail } = event as VidScrollEventMap["vidscroll-loader"];
console.log(detail?.phase);
});Related
- Plain HTML: adding the element to a page.
- vidscroll/core: the controller behind it.