Video Player
A framed video player with hover-revealed custom transport controls.
- Status
- 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-playerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/video-playerRun 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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
src* | string | — | Video source URL. |
aspectRatio | "auto" | "square" | "video" | 'video' | Reserved frame aspect ratio. |
className | string | — | Classes applied to the outer player container. |
controlsVisible | boolean | — | 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. |
defaultPlaybackRate | number | 1 | Initial playback rate applied when the video element mounts. |
defaultQuality | string | — | Initial quality label selected in the settings menu (the first option when omitted). |
description | React.ReactNode | — | Optional visible description shown below the title. |
downloadHref | string | — | 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. |
downloadLabel | string | "Download" | The label of the Download button beside loadErrorLabel. |
exclusive | boolean | true | One 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:ss | Format elapsed and duration labels. |
label | string | 'Video' | Accessible label used by the video element and custom controls. |
loadErrorLabel | string | "Can’t play this video here" | The message the frame shows when the video cannot play. |
mediaRef | React.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. |
playbackRates | readonly number[] | [0.75, 1, 1.25, 1.5, 2] | Playback rates cycled by the rate control. |
poster | string | — | 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. |
qualityOptions | readonly string[] | [] | Selectable quality labels shown in the settings menu. Empty hides the Quality entry: pass
labels only when onQualityChange really switches renditions. |
ref | React.Ref<HTMLDivElement> | — | Ref for the outer player container. |
skipSeconds | number | 15 | Seconds moved by the rewind and forward actions. |
title | React.ReactNode | — | Optional visible title shown above the video frame. |
videoClassName | string | — | Classes applied to the native <video> frame. |
Data attributes and CSS variables on VideoPlayer
| Attribute | Values |
|---|---|
data-aspect-ratio | mirrors 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
| Key | Action |
|---|---|
| Tab | Move through play/pause, seek, mute, settings, and fullscreen |
| Enter / Space | Activate the focused button |
| Space | Play or pause when the player or media controls group is focused |
| K | Play 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 |
| J | Rewind 15 seconds |
| L | Forward 15 seconds |
| M | Toggle mute |
| F | Toggle 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 |
| Home | Seek to the beginning |
| End | Seek 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 ownaria-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
qualityOptionsare 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
downloadattribute. - 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. SeeMediaPlayerControls§ 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.
| Contract | States tested |
|---|---|
| Behaviour | default, playing, paused, muted, seeking, keyboard-shortcuts, playback-rate, volume, fullscreen, fullscreen-exit, hover-fade-controls, quality-opt-in, poster, load-error, source-renewal |
| Accessibility | focus-visible, keyboard-operable, labeled, semantic-html, alert |
| Visual | default, with-copy, aspect-ratio, progress-rest, progress-hover, volume-open, error |