Video
VideoEngine makes a single <video> considerate. It watches that one element and:
- Pauses it when it scrolls out of view, then resumes it when it comes back — unless the user paused it deliberately, in which case it stays paused.
- Respects
prefers-reduced-motion— disables looping and pauses the video if it runs longer than five seconds, reacting live as the setting changes.
It only drives native playback (play() / pause()) and the loop property, so your <video> keeps
its own controls, muted, poster and styling. The behaviour is written once and shared across
every framework.
Pause it yourself and scroll away — it stays paused on the way back.
- Threshold
- 0.4 · default 0
- Auto-paused
- 0×
- Auto-resumed
- 0×
- Last reason
- —
This demo sets threshold=0.4 (resume once 40% is visible). The engine default is 0 — a single visible pixel.
Honoring playback and loop.
Pause it yourself and scroll away — it stays paused on the way back.
- Threshold
- 0.4 · default 0
- Auto-paused
- 0×
- Auto-resumed
- 0×
- Last reason
- —
This demo sets threshold=0.4 (resume once 40% is visible). The engine default is 0 — a single visible pixel.
Honoring playback and loop.
Scroll the video out of the panel and back. Pause it yourself first to see it stay paused. Toggle Simulate to preview the reduced-motion policy without changing your OS setting.
Usage
Point the hook at a single <video> and it takes over the polite-playback behaviour. An autoplay,
muted, looping hero becomes “play only while it’s on screen”:
import { useRef } from 'react';
import { useVideo } from '@60fps/react/video-engine';
function Hero() {
const ref = useRef<HTMLVideoElement>(null);
useVideo(ref);
return <video ref={ref} src="/hero.mp4" muted loop autoPlay playsInline />;
}
<script setup lang="ts">
import { ref } from 'vue';
import { useVideo } from '@60fps/vue/video-engine';
const video = ref<HTMLVideoElement | null>(null);
useVideo(video);
</script>
<template>
<video ref="video" src="/hero.mp4" muted loop autoplay playsinline />
</template>
How the viewport rule works
The engine tracks who paused the video so it never fights the user:
- If the video is playing when it leaves the viewport, the engine pauses it and resumes it on re-entry.
- If the user paused it (via the controls), it is left alone — it will not auto-resume when it scrolls back in.
- A video that was already paused/idle is never auto-played.
If the user takes over — pressing play again — the engine hands back control and re-acquires it only on the next time the video leaves the viewport while playing.
The reduced-motion rule
When prefers-reduced-motion: reduce is active, the engine:
- sets
loop = false(restoring the original value when the preference is lifted), and - pauses the video when it is longer than
reducedMotionMaxDuration(5 seconds by default). A shorter, decorative clip keeps playing.
Duration is read once metadata is available, and the whole policy re-applies live when the media query flips — no reload required.
useVideo(ref, options?)
| Option | Type | Default | Description |
|---|---|---|---|
threshold | number | number[] | 0 | IntersectionObserver threshold for the in-view check. |
rootMargin | string | '0px' | IntersectionObserver root margin. |
root | Element | Document | null | — | IntersectionObserver root. Defaults to the viewport. |
autoPlay | boolean | false | Start playback (engine-driven, once in view) instead of the native autoplay attribute. |
pauseOutOfView | boolean | true | Pause off-screen videos and resume them on re-entry. |
respectReducedMotion | boolean | true | Apply the reduced-motion policy (disable loop, pause long videos). |
reducedMotionMaxDuration | number | 5 | Seconds above which reduced motion pauses a video. |
forceReducedMotion | boolean | — | Force the policy on/off, ignoring the media query. Omit to follow the OS setting. |
To react to the engine, subscribe to its events with engine.on(...) (see below) — the hook exposes no
on* callback props.
Engine events
The hook returns the engine — an emitter. Subscribe with engine.on(event, cb) to react to it, and
engine.off(event, cb) to clean up:
| Event | Type | Default | Description |
|---|---|---|---|
visibility | (visible, entry) | — | The video entered or left the viewport. |
autopause | (reason) | — | The engine paused the video on its own. |
autoplay | (reason) | — | The engine resumed the video on its own. |
reducedmotionchange | (reduced) | — | The effective reduced-motion state changed. |
Driving reduced motion yourself
Wire the policy to your own settings UI with engine.setReducedMotion(forced) — pass true/false to
override the media query, or null to fall back to it:
const engine = useVideo(ref);
// e.g. a "reduce animations" switch in your app
const onToggle = (enabled: boolean) => engine.setReducedMotion(enabled ? true : null);