Skip to content
60fps/ui

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.

Scroll inside the panel ↓
Out of viewPaused
↑ scroll down to the video

Pause it yourself and scroll away — it stays paused on the way back.

↓ scroll up to the video
Viewport
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.

Reduced motionOff

Honoring playback and loop.

Duration
Scroll inside the panel ↓
Out of view Paused
↑ scroll down to the video

Pause it yourself and scroll away — it stays paused on the way back.

↓ scroll up to the video
Viewport
Threshold
0.4 · default 0
Auto-paused
Auto-resumed
Last reason

This demo sets threshold=0.4 (resume once 40% is visible). The engine default is 0 — a single visible pixel.

Reduced motion Off

Honoring playback and loop.

Duration —

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);