Video: posters, sources, captions, and events

esp-video wraps a native <video> element and provides poster display, lazy loading, aspect-ratio sizing, captions, controls, and loading and error states that follow the theme.

The component uses native browser playback. It does not bundle an adaptive streaming library. Provide fallback-src when a target browser cannot play your primary source. This guide explains source selection, loading, events, and accessibility setup.

Try it

The sample below is a six-second MP4 with English captions. Nothing but the poster has loaded yet. Press play (or Tab to the button and press Enter) and watch the events arrive.

Events: none yet

<esp-video
  src="/assets/esp-video/espalier-sample.mp4"
  type="video/mp4"
  poster="/assets/esp-video/espalier-sample-poster.jpg"
  poster-alt="A trellis of pale lines drifting over a green ground"
  playsinline
>
  <track kind="captions" src="/assets/esp-video/espalier-sample.en.vtt" srclang="en" label="English" default />
</esp-video>

What loads, and when

A page of videos must not cost a page of video downloads. With the default preload="none", an esp-video with a poster makes exactly one request before the visitor interacts: the poster image. No playlist, no media segments, and no caption files. A browser fixture holds the element to this by counting the page's network requests.

Setting Before play intent On play intent
preload="none" (default) The poster The source (src or fallback-src), then captions
preload="metadata" or "auto" The poster, plus metadata or media if the browser plays type Playback
defer-offscreen Nothing, not even the poster, until within 200px of the viewport As above
autoplay Starts once admitted, as though the visitor had pressed play —

A source the browser cannot play never preloads: the choice of fallback-src over it waits for play intent whatever preload says. A preloaded source that fails is different: the element switches to fallback-src at once and fires esp-video-error, before any interaction.

Choosing a source

type defaults to an HLS master playlist (application/vnd.apple.mpegurl). On the first play intent the element picks what to play, in this order:

  1. src. If video.canPlayType(type) is non-empty, the element sets src on its video. Safari, iOS, and current Chrome play HLS this way.
  2. Fallback. Otherwise, if fallback-src is set, the element plays that MP4. Firefox takes this path for an HLS src.
  3. Error. Otherwise the element shows its error state and fires esp-video-error with reason: "unsupported".

With no src at all, the element skips the first step: it plays fallback-src if one is set, and otherwise fires esp-video-error with reason: "unsupported" and the message "No video source is set."

Espalier attaches no streaming library. For a source every browser plays, set type="video/mp4" (or the file's own type), as the sample above does.

Captions

Give the element <track> children. A media element only reads its own child tracks, and a shadow-DOM <video> cannot see slotted ones, so the element copies them into its video when it attaches the source; kind, srclang, label, and default carry through. With the default preload="none" that happens on play intent, so no caption file loads before it; preload="metadata" or "auto" attaches a natively playable source early, and its tracks with it. Tracks added or removed later are copied again.

Captions served from another origin need CORS: set crossorigin on the element (crossorigin or crossorigin="anonymous", or crossorigin="use-credentials"), and it passes through to the video.

Autoplay

autoplay starts playback once the element is admitted: at once, or near the viewport with defer-offscreen. Autoplay requires muted, and the element enforces it: while autoplay is set the video is muted whatever muted says, so a page never starts sound unprompted. Removing autoplay or muted later unmutes only a paused video; a playing one stays muted until the visitor unmutes it through the controls.

Autoplay does not start for a visitor who prefers reduced motion, and a browser may block it anyway. In both cases the poster and play button stay, and esp-video-error does not fire: a blocked autoplay is not a failure. On iOS, inline autoplay also needs playsinline.

Events

All three events are composed and bubbling, so a page-level listener hears every video, including those inside other components' shadow roots. The element records no analytics itself; listen and forward what you need.

Event When detail
esp-video-ready Once per attached source, when its metadata has loaded { src, mode, duration }
esp-video-play Every time playback starts, or resumes after a pause (on the media's playing). Recovery from buffering or a seek is not reported, and a source that fails first reports no play { src, mode, currentTime, autoplay }
esp-video-error When a source fails { src, mode, reason, message, fallback }
  • src is the element's src, even while the fallback plays.
  • mode is how the source was attached: "native" for src, or "fallback". On an error it is the attachment that failed, or null when nothing was attached.
  • duration is in seconds, and Infinity for a live stream.
  • autoplay is true only when autoplay started this playback.
  • reason is "unsupported" or "media"; message is for developers and is not shown to visitors.
  • fallback is true when the element is already trying fallback-src.
document.addEventListener("esp-video-play", (event) => {
  const { src, mode, autoplay } = event.detail;
  analytics.track("video_play", { src, mode, autoplay });
});

ESP_EVENTS.VIDEO_READY, VIDEO_PLAY, and VIDEO_ERROR name the events, and EspalierVideoEventMap types them.

Accessibility

  • The play affordance over the poster is a real <button>. Its accessible name is label when set, otherwise Play video: followed by poster-alt, otherwise Play video. The poster image itself is decorative, so a screen reader hears the name once.
  • Focus is visible on the play disc and on the video, drawn with --esp-video-focus-outline (the theme's link ink by default). When playback starts from the button, focus moves to the video. When playback fails, before or after it started, focus on the button or the video moves to the error message, and the video leaves the tab order and drops its controls until the source changes. Either way keyboard users keep their place.
  • Loading is announced through a polite status message, and an error appears as a short alert over the poster, never as a blank box. Slot your own wording into slot="error" to localize it.
  • The poster's fade and the loading spinner stop under prefers-reduced-motion, and autoplay does not start.
  • controls="none" leaves a visitor no way to pause. Reserve it for short, muted, decorative loops.

Layout

The element is a block box with aspect-ratio (16 / 9 by default; any positive number or number / number, validated exactly as esp-image validates ratio). It fills whatever width the layout gives it, so it works in a page column, a grid cell, or a full-bleed esp-section, with no special cases. The video letterboxes inside the box (--esp-video-object-fit: contain) while the poster covers it.

Theming

Every color and size goes through --esp-video-* tokens, listed in the styling reference: the surface, the corner radius, the object fit, the controls' accent, the focus outline, the play disc's size, fill, and glyph, and the error message's colors.

<esp-video
  src="/assets/esp-video/espalier-sample.mp4"
  type="video/mp4"
  poster="/assets/esp-video/espalier-sample-poster.jpg"
  poster-alt="The trellis sample, themed"
  style="--esp-video-play-background: var(--esp-color-primary); --esp-video-play-color: white; --esp-video-border-radius: 0;"
></esp-video>

--esp-video-accent sets accent-color on the native controls. Browsers decide how much of their media controls follow it, so treat it as a hint. Custom control chrome is planned as a follow-up.

Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O