Skip to content
Component installs need the registry setup
VegaStack Design

Video Player

A framed video player with hover-revealed custom transport controls.

Status
stable
Since
0.5.0
Accessibility pattern
native media controls

Last updated

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Video Player before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify video-player

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/video-player

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/button, @vegastack/media-player-controls, @vegastack/use-media-query.

Usage

import { VideoPlayer } from "@/components/ui/video-player";

<VideoPlayer
  src="/media/demo.mp4"
  poster="/media/demo-poster.webp"
  label="Product demo video"
/>;

Examples

Basic

The video surface owns a hover-revealed overlay transport: a thin media progress rail that expands smoothly on hover or focus, then play/pause, time, volume, settings, and fullscreen in the lower row. poster shows in the frame until playback starts — store one at upload with probeVideo from the media-probe lib — and the player never autoplays. Settings holds playback speed; a Quality entry appears only when you pass qualityOptions (and switch renditions in onQualityChange), since a single stored file has one. The docs preview uses a 90-second local MP4 fixture, so it can play without network access.

With copy

Use title and description when a video needs a visible name or supporting context.

Product walkthrough
A video frame using the same transport as the audio player.

One at a time

Starting the video pauses any other DS player that is playing — an AudioPlayer, the global player, the FileViewer stage — and starting one of them pauses the video. Nothing resumes by itself. exclusive={false} opts it out both ways.

Aspect ratio

aspectRatio="video" reserves a 16:9 frame. Use square for square media or auto when the video element should keep its intrinsic height.

Can't play

A browser cannot play every video: an HEVC .mov in Chrome or Firefox, an .mkv, an .avi. On a media error the frame shows "Can’t play this video here" (loadErrorLabel) with a Download button to downloadHref, and the transport — which would do nothing — goes away. Without downloadHref the message shows alone.

Expired URLs

A signed URL expires; a video paused past its lifetime fails on the next seek. Pass onSourceExpired and, on the first media error, the player calls it once, loads the URL it resolves and resumes at the same position — playing, if it was. Returning the same URL (a route that re-signs on every request) reloads it. A second failure shows the "can't play" card. This is the same contract as AudioPlayer's onSourceExpired.

<VideoPlayer
  src={`/api/files/${file.id}`}
  poster={file.posterUrl}
  label={file.name}
  downloadHref={`/api/files/${file.id}?download`}
  onSourceExpired={async () => `/api/files/${file.id}`}
/>

Controls always visible

The overlay reveals on pointer or focus and fades a second after the pointer leaves. controlsVisible takes that out of the loop — true pins the chrome open, false keeps it closed. Use it for a kiosk or always-on player, and for a static fixture: the auto-reveal never fires without a pointer, so a screenshot or a contract check would otherwise capture a frame with no controls in it.

Playground

Every VideoPlayer prop, auto-generated from its TypeScript types.

API Reference

PropTypeDefaultDescription
src*string—Video source URL.
aspectRatio"auto" | "square" | "video"'video'Reserved frame aspect ratio.
classNamestring—Classes applied to the outer player container.
controlsVisibleboolean—Force the overlay controls open (true) or closed (false), taking the auto hide/reveal out of the loop. Leave it undefined for the default behaviour: reveal on pointer or focus, fade out a second after the pointer leaves; on a coarse pointer (a touch screen, which has no hover) they stay shown, so the play button is always there. Use true for a kiosk/always-on player — and for a static docs or test fixture, which is what lets the contract lane see the chrome at all.
defaultPlaybackRatenumber1Initial playback rate applied when the video element mounts.
defaultQualitystring—Initial quality label selected in the settings menu (the first option when omitted).
descriptionReact.ReactNode—Optional visible description shown below the title.
downloadHrefstring—The file's download URL. When the browser cannot play the video (an HEVC .mov, an .mkv or .avi), the frame shows loadErrorLabel with a Download button to this URL.
downloadLabelstring"Download"The label of the Download button beside loadErrorLabel.
exclusivebooleantrueOne media at a time: starting this player pauses any other DS player that is playing (AudioPlayer, VideoPlayer, GlobalAudioPlayer, the FileViewer stages), and starting one of those pauses this one. false opts out both ways. Nothing resumes by itself.
formatTime((seconds: number) => string)mm:ss / h:mm:ssFormat elapsed and duration labels.
labelstring'Video'Accessible label used by the video element and custom controls.
loadErrorLabelstring"Can’t play this video here"The message the frame shows when the video cannot play.
mediaRefReact.Ref<HTMLVideoElement>—Ref for the native <video> media engine.
onPlaybackRateChange((playbackRate: number) => void)—Called whenever the playback rate changes.
onPlayStateChange((playing: boolean) => void)—Called whenever playback starts or pauses.
onQualityChange((quality: string) => void)—Called whenever the selected quality label changes.
onSourceExpired(() => Promise<string>)—Renew an expired source. When the video fails to load after it had a URL — a signed URL that has expired — the player calls this once, loads the URL it resolves, and resumes at the same position (playing, if it was). A second failure, or a rejection, shows the "can't play" card; a new src re-arms it. The same contract as AudioPlayer's onSourceExpired.
onTimeChange((currentTime: number, duration: number) => void)—Called whenever the current playback time changes.
playbackRatesreadonly number[][0.75, 1, 1.25, 1.5, 2]Playback rates cycled by the rate control.
posterstring—An image shown in the frame until playback starts — a poster frame stored with the upload (probeVideo grabs one). It also sits behind the "can't play" card.
qualityOptionsreadonly string[][]Selectable quality labels shown in the settings menu. Empty hides the Quality entry: pass labels only when onQualityChange really switches renditions.
refReact.Ref<HTMLDivElement>—Ref for the outer player container.
skipSecondsnumber15Seconds moved by the rewind and forward actions.
titleReact.ReactNode—Optional visible title shown above the video frame.
videoClassNamestring—Classes applied to the native <video> frame.

Data attributes and CSS variables on VideoPlayer

AttributeValues
data-aspect-ratiomirrors a prop or state value
data-icon"inline-start"
data-slot"video-player" | "video-player-controls-overlay" | "video-player-controls-scrim" | "video-player-error" | "video-player-frame" | "video-player-header" | "video-player-media"
data-state"error" | "hidden" | "visible"

Accessibility

KeyAction
TabMove through play/pause, seek, mute, settings, and fullscreen
Enter / SpaceActivate the focused button
SpacePlay or pause when the player or media controls group is focused
KPlay or pause
←Rewind 15 seconds when the player or media controls group is focused
→Forward 15 seconds when the player or media controls group is focused
JRewind 15 seconds
LForward 15 seconds
MToggle mute
FToggle fullscreen when the player is focused
↑ / ↓Change volume while the volume slider is focused
← / ↓Decrease the seek position by one second while the slider is focused
→ / ↑Increase the seek position by one second while the slider is focused
HomeSeek to the beginning
EndSeek to the end
  • Pass a descriptive label; it names the video element and prefixes every custom control.
  • The named media progress rail composes Slider, so keyboard and screen-reader slider semantics stay intact while its video-specific hover treatment remains separate from the generic component.
  • Icon-only transport buttons are Button size="icon", each carrying its own aria-label.
  • Tooltips expose the primary keyboard shortcuts for pointer and keyboard users.
  • The volume slider is exposed from the mute control and keeps slider keyboard semantics.
  • Playback speed (and quality, when qualityOptions are passed) live in settings submenus with radio selection.
  • A video that cannot play announces "Can’t play this video here" as an alert, and its Download button is a real link with the download attribute.
  • Rewind and forward are keyboard shortcuts rather than visible buttons.
  • The overlay controls appear on hover or focus and fade out after the pointer leaves the frame.
  • The fullscreen action changes to an exit-fullscreen icon and accessible name while the frame owns document fullscreen. The transport's tooltips and its settings menu stay visible there: they portal into the fullscreen element rather than to <body>, which is decision OVL-14. See MediaPlayerControls § Accessibility.
  • The native <video> element is the media engine and visual frame; browser-native controls are omitted so audio and video players share the same VegaStack transport.
ContractStates tested
Behaviourdefault, playing, paused, muted, seeking, keyboard-shortcuts, playback-rate, volume, fullscreen, fullscreen-exit, hover-fade-controls, quality-opt-in, poster, load-error, source-renewal
Accessibilityfocus-visible, keyboard-operable, labeled, semantic-html, alert
Visualdefault, with-copy, aspect-ratio, progress-rest, progress-hover, volume-open, error

Do / Don't

Do
Use VideoPlayer for a single video asset that needs a branded frame and standard transport controls.
Don't
Use VideoPlayer for playlists, caption editing, or multi-source adaptive streaming controls.

On this page