Skip to main content

Video

Video

Video player with themed controls and full keyboard support

Tags: component media display

View Demo

Video replaces the browser's unstyleable native controls with a themed control bar, without giving up any playback capability. Every control is keyboard reachable, and the whole player responds to the shortcuts viewers expect from a video site.

Import​

<script>
  import { Video } from '@classic-homes/theme-svelte';
</script>

Basic Usage​

<Video
  src="/walkthrough.mp4"
  poster="/walkthrough.jpg"
  label="Product walkthrough"
  tracks={[{ src: '/walkthrough.en.vtt', srclang: 'en', label: 'English', default: true }]}
/>

Props​

Prop Type Default Description
item VideoMediaItem - Supplies src, poster, tracks, and the label in one object
src string | MediaSource[] - A single URL, or several sources for format fallback
poster string - Image shown before playback starts
tracks MediaTextTrack[] [] Caption and subtitle tracks
label string 'Video player' Accessible name for the player region
ratio 'square' | 'video' | 'photo' | 'portrait' | 'wide' | number 'video' Aspect ratio; pass null to size from the content
fit 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' 'contain' How the video fills its box
autoplay boolean false Start playing automatically; implies muted
muted boolean false Start muted
loop boolean false Loop playback
volume number 1 Initial volume, 0 to 1
preload 'none' | 'metadata' | 'auto' 'metadata' How much to preload
hideControls boolean false Hide the control bar entirely
showPictureInPicture boolean true Show the picture-in-picture button
showFullscreen boolean true Show the fullscreen button
onEnded () => void - Called when playback ends
onTimeUpdate (currentTime: number) => void - Called on each time update, with seconds elapsed
hlsLoader () => Promise<HlsConstructor> - Per-player hls.js loader, overriding the registered one
hlsConfig Record<string, unknown> - Extra hls.js configuration
class string - Additional CSS classes
overlay Snippet - Content overlaid on the video, above the controls

Snippets​

Snippet Description
overlay Content layered over the video, above controls

Keyboard Shortcuts​

Active whenever focus is anywhere inside the player. Shortcuts are ignored while a text field has focus, and while a modifier key is held, so browser chords keep working.

Key Action
Space or K Play/pause
← / → Seek 5 seconds
J / L Seek 10 seconds
↑ / ↓ Volume up/down
M Mute/unmute
F Toggle fullscreen
C Toggle captions
Home / End Jump to start/end
0–9 Seek to 0%–90%

Multiple Sources​

Give several sources to let the browser pick the first format it can decode:

<Video
  src={[
    { src: '/clip.webm', type: 'video/webm' },
    { src: '/clip.mp4', type: 'video/mp4' },
  ]}
  tracks={captions}
/>

Captions​

Pre-recorded video needs captions to meet WCAG 1.2.2. The component warns in development when tracks is empty, and the captions button only appears once at least one track exists.

<Video
  src="/clip.mp4"
  tracks={[
    { src: '/clip.en.vtt', srclang: 'en', label: 'English', default: true },
    { src: '/clip.es.vtt', srclang: 'es', label: 'Español' },
  ]}
/>

Autoplay​

Every major browser blocks autoplay with sound. Setting autoplay therefore mutes the video automatically, and a development warning fires if you also set muted={false}.

Adaptive Streaming (HLS)​

Video plays HLS playlists with the same controls as a progressive file. Safari and iOS play HLS natively; every other browser needs hls.js (opens in new tab), which is an optional peer dependency.

Install it and register a loader once, at application startup:

npm install hls.js
// src/routes/+layout.svelte, or wherever your app boots
import { setHlsLoader } from '@classic-homes/theme-svelte';

setHlsLoader(() => import('hls.js').then((m) => m.default));

The import deliberately lives in your code rather than inside this package. A dynamic import('hls.js') written in the library would be resolved at build time by every consumer's bundler, so anyone who had not installed hls.js would fail to build even if they never played a stream.

A global Hls — as set by the library's CDN build in a <script> tag — is detected automatically and needs no registration. Individual players can also take an hlsLoader prop.

Without any of these, a browser that cannot play HLS natively shows an inline explanation rather than failing silently.

<Video src="https://example.com/playlist.m3u8" label="Live stream" />

Sources are treated as HLS when they declare application/vnd.apple.mpegurl or end in .m3u8. Progressive sources listed alongside a playlist are kept as fallbacks.

preload="none" is honoured for streams too. hls.js downloads through its own loader and ignores the element's preload, so the player holds its loader back until the viewer presses play — without this, every stream on a page would pull segments the moment it mounted and starve the others of bandwidth. Pass hlsConfig={{ autoStartLoad: true }} to override.

Cloudflare Stream​

cloudflareStream() builds a playable item from a Stream video id, deriving both the HLS playlist and a poster image from Stream's URL scheme — no API call required.

<script lang="ts">
  import { Video, cloudflareStream } from '@classic-homes/theme-svelte';

  const tour = cloudflareStream('a1b2c3d4e5f6', {
    customerCode: 'abc123',
    title: 'Product tour',
  });
</script>

<Video item={tour} />
Option Type Default Description
customerCode string - Customer code from the Stream dashboard; required unless domain
domain string - Custom hostname serving Stream
title string - Title used as the player's accessible name
caption string - Caption shown beneath the video
poster string - Override the derived poster
posterTime string '1s' Timestamp the derived poster is taken from
posterHeight number 720 Height of the derived poster
duration number - Known duration, shown before metadata loads
downloadable boolean false Expose the MP4 download URL (requires downloads enabled)
lowLatency boolean false Request the low-latency HLS variant
clientBandwidthHint number - Bandwidth hint in Mbps

Because Stream substitutes a signed playback token wherever the video id would appear, private videos need no separate code path — pass the token as the id:

const item = cloudflareStream(signedToken, { customerCode: 'abc123' });

Stream embeds its captions in the playlist itself, so the captions button populates without a tracks prop, and the missing-captions development warning is suppressed for HLS sources.

For lower-level use, cloudflareStreamUrls() returns the hls, dash, iframe, thumbnail, and download URLs, and cloudflareStreamThumbnail() builds still or animated thumbnails with Stream's time, height, width, and fit parameters.

To embed Cloudflare's own player instead of these controls, use MediaEmbed with provider="cloudflare".

Accessibility​

  • The player is a labelled region and is focusable, so the shortcuts have somewhere to land.
  • Controls auto-hide only while playing; they stay visible when paused, hovered, or focused within, and hiding is disabled entirely under prefers-reduced-motion.
  • The seek bar announces spoken timestamps (1 minute 23 seconds of 4 minutes) rather than raw numbers.
  • Play, mute, captions, and fullscreen buttons all relabel themselves to match their current state.
  • Audio - The same player, laid out for audio
  • MediaEmbed - YouTube and Vimeo without a self-hosted file
  • Lightbox - Plays video full-screen
  • MediaCarousel - Video among other media types
On this page