VideoPlayer

media

DS-native video via Vidstack — CDN MP4, YouTube, and Vimeo URLs with one player API

Desktop autoplays muted or click-to-play; mobile shows poster still — no video downloaded

YouTube/Vimeo on touch or narrow viewports can use optional `nativeFallbackUrl` (MP4) instead of the embed for reliable tap-to-play

Exposes imperative handle (play/pause/restart/seek) for scroll-driven or external control

import { VideoPlayer } from "@everydayos/ui";

Usage

Silent autoplay behind slot content — no controls.

<VideoPlayer
  source={{ kind: "cdn", cdnUrl: "/video/bg.mp4", posterUrl: "/poster.jpg" }}
  behavior="background"
  controls="none"
  loop
/>

API

behavior
autoplayMutedbackgroundclickToPlay
defaultpreferredrare
controls
defaultminimalnone
defaultpreferredrare
ambientGlass
glassnone
PropTypeDefault
source*VideoSource
loopboolean
restartOnPlayboolean
onInitialPlaybackStart() => void
classNamestring

Examples

Click-to-play with poster

Shows poster still + play button; clicking starts from t=0.

<MediaFrame aspect="video" radius="sm">
  <VideoPlayer
    source={{ kind: "cdn", cdnUrl: "/video/film.mp4", posterUrl: "/poster.jpg" }}
    behavior="clickToPlay"
    controls="minimal"
    restartOnPlay
  />
</MediaFrame>

Loop preview + click-to-play

Ambient loop on desktop, poster on mobile; click plays full video.

<MediaFrame aspect="video" radius="sm">
  <VideoPlayer
    source={{
      kind: "cdn",
      cdnUrl: "/video/film.mp4",
      posterUrl: "/poster.jpg",
      loopPreviewUrl: "/video/preview-loop.mp4",
    }}
    behavior="clickToPlay"
    controls="minimal"
  />
</MediaFrame>

YouTube (click-to-play)

Studio: YouTube URL + Mobile / touch MP4 strongly recommended; touch uses native playback when `nativeFallbackUrl` is set.

<MediaFrame aspect="video" radius="sm">
  <VideoPlayer
    source={{
      kind: "youtube",
      externalUrl: "https://www.youtube.com/watch?v=VIDEO_ID",
      nativeFallbackUrl: "/video/same-film.mp4",
      posterUrl: "/poster.jpg",
    }}
    behavior="clickToPlay"
    controls="minimal"
  />
</MediaFrame>

Externally controlled (scroll expand)

forwardRef handle for imperative play/pause from a scroll wrapper.

const playerRef = useRef<VideoPlayerHandle>(null);

<VideoPlayer
  ref={playerRef}
  source={{ kind: "cdn", cdnUrl: "/video/hero.mp4" }}
  behavior="background"
/>

// From scroll wrapper:
playerRef.current?.restart();
playerRef.current?.pause();

Composition

Pairs well with

MediaFrame
GlassPill
Layer
Text

When to avoid

  • You want a background image — use MediaFrame with <Image fill />
  • You cannot ship client-side JS — VideoPlayer is a client component with Vidstack
  • You want a full-page video hero with text overlay — use v2Cell background:video + backgroundVideo

Patterns

Video in a slot (VideoPlayerShape)

MediaFrame
VideoPlayer
Text

The videoPlayer v2 shape wraps VideoPlayer in a MediaFrame with -mx-content-inset so it hugs cell edges, matching MediaShape behaviour.

<div className="-mx-content-inset">
  <MediaFrame aspect="video" radius="sm">
    <VideoPlayer source={source} behavior="clickToPlay" controls="minimal" />
  </MediaFrame>
</div>

Cell backdrop video (backgroundVideo)

VideoPlayer
Layer

VideoPlayer used as full-bleed background on a v2Cell. Slot content (textGroup, etc.) renders above it via Layer. Mode background = silent loop; mode player = play button over slots.

{/* CellRenderer renders this as absolute fill, slots on top */}
<VideoPlayer
  source={cell.backgroundVideo.source}
  behavior="background"
  controls="none"
  loop
/>

Mobile click-to-play

VideoPlayer
VideoPlayerPoster

On small viewports, autoplay background/minimal modes stay poster-only to save data. Click-to-play still mounts <video> with playsInline; the poster (or empty placeholder) stays on top until the user taps play so playback can start in a user gesture.

<VideoPlayer
  source={{ kind: "cdn", cdnUrl: "...", posterUrl: "/poster.jpg" }}
  behavior="clickToPlay"
/>
{/* On mobile: shows <VideoPlayerPoster> only, no <video> element created */}