vidscroll0.3.3

<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

AttributeValuesMeaning
srcURLVideo file. Without it the element does nothing; removing it stops the video.
length“400vh”, “2000px”, “auto”Scrolling that covers the whole video.
easingpreset nameCurve from scroll to video time, such as inOutSine.
fitcover, containHow the video fills the screen. Default cover.
posterURL, or noneImage while loading. Without it, the video’s first frame is shown.
smooth-scrollpresent, 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.
fpsnumberFrame rate for frame-based section ranges.
warmup“false”, or a number of stepsThe pass over the timeline while loading.
loadernoneHide the loading overlay and badge.
debugpresentLog 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:

main.js
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:

AttributeMeaning
start, endScroll progress, 0 to 1
from-time, to-timeSeconds of video
from-frame, to-frameFrame numbers
idName 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

Scroll inside the preview
index.html
<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>
MemberWhat 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.
apiThe full controller, or null before the element has started. See vidscroll/core.
optionsObject 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.

EventFired ondetail
vidscroll-loadelement{ source, probe }: how the video was served
vidscroll-readyelementThe current state. Fires again if the scroll mapping is rebuilt.
vidscroll-loaderelementThe loading state, or null when loading is done
vidscroll-errorelementThe Error
vidscroll-entersection{ id }
vidscroll-exitsection{ 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:

HookWhat 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-progressLoading progress from 0 to 1, on the element
–video-progressScroll progress through the whole video, on the element
styles.css
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:

index.html
<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 length plus one screen, set when it starts. Set length explicitly when content follows it.
  • A parent with overflow: hidden, auto or scroll stops the video from pinning. Use overflow: clip instead.
  • 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:

main.ts
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);
});