Skip to content

Video

import { Video } from '@delightstack/components';
import type { Source, Track } from '@delightstack/components';
0:00 / 0:00
View code
<script>
import { Video } from '@delightstack/components';
</script>
<Video src="/videos/intro.mp4" poster="/videos/intro-poster.jpg" />

Provide multiple sources with resolution labels for quality switching.

<script>
import { Video } from '@delightstack/components';
import type { Source } from '@delightstack/components';
const sources: Source[] = [
{ src: '/videos/demo-1080.mp4', type: 'video/mp4', size: 1080 },
{ src: '/videos/demo-720.mp4', type: 'video/mp4', size: 720 },
{ src: '/videos/demo-480.mp4', type: 'video/mp4', size: 480 },
];
</script>
<Video src={sources} poster="/videos/demo-poster.jpg" />

Point src at an .m3u8 playlist (or pass a source with type application/vnd.apple.mpegurl) and the player handles HLS automatically. Browsers with native HLS support (Safari, iOS) play it directly; everywhere else the player lazy-loads hls.js only when an HLS source is detected. The custom controls — including a manifest-driven quality menu with an Auto option — are used in both cases.

hls.js is an optional peer dependency. Install it for HLS support in non-Safari browsers:

Terminal window
pnpm add hls.js
0:00 / 0:00
View code
<Video src="https://stream.example.com/playlist.m3u8" />

Add subtitle or caption tracks.

<script>
import { Video } from '@delightstack/components';
import type { Track } from '@delightstack/components';
const captions: Track[] = [
{ kind: 'subtitles', src: '/subs/en.vtt', srclang: 'en', label: 'English', default: true },
{ kind: 'subtitles', src: '/subs/es.vtt', srclang: 'es', label: 'Spanish' },
];
</script>
<Video src="/videos/lecture.mp4" {captions} />

Auto-playing videos must be muted to comply with browser policies.

Autoplay requires muted ; browsers block autoplay-with-sound until the user interacts.

View code
<Video src="/videos/background.mp4" autoplay muted loop controls={false} />

Bind the native HTMLVideoElement for programmatic control.

<script>
import { Video } from '@delightstack/components';
let player = $state<HTMLVideoElement>();
</script>
<Video src="/videos/clip.mp4" bind:player onready={({ player: p }) => {
console.log('Video ready, duration:', p.duration);
}} />
<button onclick={() => player?.play()}>Play</button>

Use skeleton while the source is still being resolved (e.g. fetched async). It fills the player with a placeholder background, skeleton controls, and a shimmer sweep. Providing a src automatically disables the skeleton — so the shimmer never sits on top of a real poster or video.

View code
<script>
import { Video } from '@delightstack/components';
// no src yet → skeleton; set it once it resolves
let src = $state('');
</script>
<Video {src} skeleton={!src} />
PropTypeDefaultDescription
srcstring | Source[]requiredVideo source URL or array of sources with quality options. .m3u8 URLs (or sources typed application/vnd.apple.mpegurl) are played as HLS
posterstringundefinedPoster image URL
autoplaybooleanfalseAuto-play (requires muted for most browsers)
mutedbooleanfalseStart muted ($bindable)
loopbooleanfalseLoop playback
controlsbooleantrueShow custom controls
aspect_ratiostring'16/9'CSS aspect ratio
preload'auto' | 'metadata' | 'none''metadata'Preload behavior
captionsTrack[][]Caption/subtitle tracks
skeletonbooleanfalseShow loading skeleton while no src is known yet (providing a src disables it)
idstringautoElement ID
classstring''Additional CSS classes
elementHTMLElementundefinedBindable reference to the root element ($bindable)
playerHTMLVideoElementundefinedBindable reference to the video element ($bindable)
titlestringundefinedShort title/caption drawn as part of the chrome — above the control bar, under the same scrim, fading with the controls. Distinct from captions, which are WebVTT subtitle tracks
EventDetailDescription
onplaynoneFires when playback starts
onpausenoneFires when playback pauses
onendednoneFires when playback ends
ontimeupdate{ currentTime: number, duration: number }Fires on time update
onerror{ error: MediaError }Fires when a playback error occurs
onenterfullscreennoneFires when entering fullscreen
onexitfullscreennoneFires when exiting fullscreen
onenterpipnoneFires when entering picture-in-picture
onexitpipnoneFires when exiting picture-in-picture
onready{ player: HTMLVideoElement }Fires when the video element is ready
interface Source {
src: string;
type: string;
size?: number;
}
interface Track {
kind: 'captions' | 'subtitles';
src: string;
srclang: string;
label: string;
default?: boolean;
}
  • Container has role="group" with aria-label="Video player"
  • tabindex="0" for keyboard focus
  • Space / K toggles play/pause
  • Arrow Left / Right seeks backward/forward 10 seconds
  • Arrow Up / Down adjusts volume
  • M toggles mute, F toggles fullscreen, C toggles captions
  • Progress bar uses role="slider" with aria-label="Seek" and aria-valuenow/aria-valuemax
  • Volume slider uses role="slider" with aria-label="Volume"
  • All control buttons have descriptive aria-label attributes
  • Quality and speed menus use aria-haspopup and aria-expanded
  • Controls auto-hide during playback and reappear on mouse/touch/keyboard activity
  • Focus-visible outlines on all interactive elements