Skip to content

joinhottub/react-poster-video

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-poster-video

Autoplay hero video that doesn't ruin your site.

A tiny (~1 component, zero runtime deps) React component for cover/hero video that treats motion as a progressive enhancement, not the baseline:

  • 🖼️ The poster image is the real header. It's the SSR markup and paints instantly. The muted loop fades in on top only once it can genuinely play — so a slow connection, a decode failure, or a blocked autoplay just leaves the photo. No black flash, no layout shift, no broken state.
  • Respects prefers-reduced-motion. The video never even mounts for those users. Accessibility-first, not bolted on.
  • 🔋 Plays only when on-screen (IntersectionObserver) and pauses when the tab is hidden — no off-screen video draining battery and bandwidth.
  • 🤝 Survives the React autoplay footgun (the one everyone hits — see below).
  • 🧩 Works in the Next.js App Router (ships the "use client" directive).
npm i react-poster-video
import { PosterVideo } from 'react-poster-video';

<PosterVideo
  poster="/hero/fountain-poster.jpg"
  src="/hero/fountain.mp4"
  alt="A stone fountain welling over basalt"
  style={{ aspectRatio: '16 / 9' }}
/>;

Multiple codecs, a custom crop, and overlay content:

<PosterVideo
  poster="/hero/falls-poster.jpg"
  sources={[
    { src: '/hero/falls.webm', type: 'video/webm' },
    { src: '/hero/falls.mp4', type: 'video/mp4' },
  ]}
  objectPosition="70% 40%"
  crossfadeMs={800}
  style={{ height: 480 }}
>
  <h1 style={{ position: 'relative', color: 'white' }}>Where strangers become regulars</h1>
</PosterVideo>

The React autoplay footgun

Browsers only allow autoplay for muted video. The catch: React's JSX muted prop can render as an attribute without setting the DOM .muted property — and the browser checks the property. So <video muted autoPlay> frequently gets its play() rejected, and you ship a black box.

react-poster-video sets .muted = true on the element imperatively and calls .play() itself, swallowing the rejection so the poster simply stays if autoplay is still refused:

const v = videoRef.current;
v.muted = true;                 // the property, not just the attribute
v.play().then(reveal).catch(() => {/* poster stays — no broken state */});

That single detail is the difference between a hero that "sometimes doesn't play" and one that's reliable.

Props

Prop Type Default
poster string Required. The static header image (paints instantly).
src string A single MP4 source.
sources { src; type? }[] Multiple sources (webm + mp4). Wins over src.
alt string '' Poster alt text.
className / style Applied to the wrapper. Set sizing here (aspectRatio/height).
objectFit CSSProperties['objectFit'] 'cover' Fit for poster + video.
objectPosition string Crop position, e.g. "70% 40%".
crossfadeMs number 600 Fade-in once the video plays.
loop boolean true Loop playback.
playWhenVisible boolean true Play only while on-screen.
rootMargin string '200px' IntersectionObserver margin.
respectReducedMotion boolean true Skip video for reduced-motion users.
onReady () => void Fires when the video is playing + revealed.
children ReactNode Overlay content (gradients, headings, CTAs).
videoProps VideoHTMLAttributes Passthrough to the <video>.

The wrapper is position: relative; overflow: hidden; the poster and video are absolutely positioned to fill it. You control the size via style/className.

Encoding the loop (the other half people get wrong)

A good hero loop is muted, web-optimized, and small. Strip the audio track, cap the size, and move the moov atom up front for fast start. ffmpeg:

# MP4 (H.264) — broad support, audio stripped, fast-start
ffmpeg -i source.mov -an -vf "scale=1600:-2" -c:v libx264 -profile:v high \
  -crf 24 -preset slow -movflags +faststart hero.mp4

# WebM (VP9) — smaller; offer it first, fall back to MP4
ffmpeg -i source.mov -an -vf "scale=1600:-2" -c:v libvpx-vp9 \
  -crf 33 -b:v 0 hero.webm

# Poster frame (grab a representative still ~1s in)
ffmpeg -i source.mov -ss 00:00:01 -frames:v 1 -q:v 3 hero-poster.jpg

Tips: keep loops short (a few seconds) and pick subjects where motion is the point — water, fire, steam, drifting clouds. A frozen frame of those reads as inert; almost everything else is better served by a plain image.

License

MIT © Hottub, Inc.

About

Poster-first autoplay hero/cover video for React — reduced-motion aware, pauses off-screen, survives blocked autoplay.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors