Poster Video
PosterVideoEngine is the Apple-homepage trick: ship only a lightweight poster <img>, and create the
real <video> lazily as the poster nears the viewport. The video is overlaid on the poster, faded
in once it can play, and then handed to a composed VideoEngine for autoplay and
off-screen pausing. Until you scroll near it, there’s no <video> element, no decoder, no bytes.
No <video> exists in the DOM until the poster nears the viewport.
The page ships with only the poster <img>. As it approaches the viewport, PosterVideoEngine creates a <video>, overlays it on the poster, and fades it in once it can play — then a composed VideoEngine autoplays it and pauses it off-screen. The poster takes its alt along.
No <video> exists in the DOM until the poster nears the viewport.
The page ships with only the poster <img>. As it approaches the viewport, PosterVideoEngine creates a <video>, overlays it on the poster, and fades it in once it can play — then a composed VideoEngine autoplays it and pauses it off-screen. The poster takes its alt along.
Scroll the poster into the panel — the <video> is created on approach, fades in, and starts
playing.
Usage
Point the hook at a poster <img> inside a positioned wrapper. Pass the video sources; the engine
does the rest.
import { useRef } from 'react';
import { usePosterVideo } from '@60fps/react/poster-video-engine';
function Hero() {
const ref = useRef<HTMLImageElement>(null);
usePosterVideo(ref, { sources: '/hero.mp4', autoPlay: true, loop: true });
return (
<div className="relative">
<img ref={ref} src="/hero-poster.jpg" alt="Product hero" className="w-full object-cover" />
</div>
);
}
<script setup lang="ts">
import { ref } from 'vue';
import { usePosterVideo } from '@60fps/vue/poster-video-engine';
const poster = ref<HTMLImageElement | null>(null);
usePosterVideo(poster, { sources: '/hero.mp4', autoPlay: true, loop: true });
</script>
<template>
<div class="relative">
<img ref="poster" src="/hero-poster.jpg" alt="Product hero" class="w-full object-cover" />
</div>
</template>
Responsive sources
A string is shorthand for a single src. Pass an array to emit <source> elements and let the browser
choose by media / type (resolution per breakpoint, codec fallback):
usePosterVideo(ref, {
sources: [
{ src: '/hero-480.mp4', type: 'video/mp4', media: '(max-width: 600px)' },
{ src: '/hero-1080.webm', type: 'video/webm' },
{ src: '/hero-1080.mp4', type: 'video/mp4' }
]
});
How it works
- A first IntersectionObserver watches the poster with a generous
preloadMargin(200px by default), so the clip is created and preloaded before it’s on screen. - On approach the engine creates the
<video>, mirrors the poster’sobject-fit/border-radius, and overlays it atopacity: 0over the poster<img>, which stays behind as the placeholder (so the<video>needs noposterof its own). - As soon as the video has a renderable frame (
loadeddata/canplay/playing, whichever fires first) it fades it in and emitsready—canplayalone is unreliable, so it isn’t trusted on its own. - A composed
VideoEnginetakes over playback — pause off-screen and theprefers-reduced-motionpolicy, plus autoplay when in view if you passautoPlay. Reach it viaengine.video.
usePosterVideo(ref, options)
Extends every VideoEngine option — forwarded as-is to the composed engine. autoPlay
is not defaulted here: the engine just lazily loads the video, so opt into playback yourself. Plus:
| Option | Type | Default | Description |
|---|---|---|---|
sources | string | VideoSource[] | — | Required. A single src, or { src, type?, media? }[] mapped to <source> elements. |
preloadMargin | string | '200px' | Root margin for the observer that creates/preloads the video ahead of the viewport. |
fadeDuration | number | 300 | Fade-in duration (ms) once the video can play. |
destroyWhenFar | boolean | false | Destroy the video when it scrolls far away and recreate it on approach, to free the decoder. |
muted | boolean | true | Required for autoplay; mirrored onto the <video>. |
loop | boolean | false | Mirrored onto the <video>. |
controls | boolean | false | Mirrored onto the <video>. |
playsInline | boolean | true | Mirrored onto the <video>. |
videoAttrs | Record<string, string | number | boolean> | — | Extra attributes for the <video> (e.g. crossorigin, preload). |
videoClass | string | — | Classes for the <video>, on top of the mirrored object-fit / border-radius. |
Engine events
The hook returns the engine — an emitter. Subscribe with engine.on(event, cb); reach the playback
engine via engine.video:
| Event | Type | Default | Description |
|---|---|---|---|
mount | (video) | — | The <video> was created and inserted over the poster. |
ready | (video) | — | The video has a frame to show and has faded in. |
unmount | () | — | The video was destroyed (only with destroyWhenFar). |