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:
src. Ifvideo.canPlayType(type)is non-empty, the element setssrcon its video. Safari, iOS, and current Chrome play HLS this way.- Fallback. Otherwise, if
fallback-srcis set, the element plays that MP4. Firefox takes this path for an HLSsrc. - Error. Otherwise the element shows its error state and fires
esp-video-errorwithreason: "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 } |
srcis the element'ssrc, even while the fallback plays.modeis how the source was attached:"native"forsrc, or"fallback". On an error it is the attachment that failed, ornullwhen nothing was attached.durationis in seconds, andInfinityfor a live stream.autoplayistrueonly whenautoplaystarted this playback.reasonis"unsupported"or"media";messageis for developers and is not shown to visitors.fallbackistruewhen the element is already tryingfallback-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 islabelwhen set, otherwisePlay video:followed byposter-alt, otherwisePlay 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, andautoplaydoes 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.