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)
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