Skip to content

ReferenceDisplay

media-thumbnail

Time-based thumbnail preview component for timeline scrubbing and hover previews

Quick Start: Video Track

<media-thumbnail> can read thumbnail cues directly from your video track. Add a <track> with kind="metadata" and label="thumbnails" to your media element.

Mux provides this as storyboard.vtt:

https://image.mux.com/{PLAYBACK_ID}/storyboard.vtt

That track is cross-origin, and a cross-origin <track> only loads when the media element is CORS-enabled:

<video src="video.mp4" crossorigin="anonymous">
  <track
    kind="metadata"
    label="thumbnails"
    src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt"
    default
  />
</video>
<media-thumbnail time="12"></media-thumbnail>

A same-origin track needs none of this.

Import

import '@videojs/html/ui/thumbnail';

Anatomy

<media-thumbnail>
  <img alt="" decoding="async" />
</media-thumbnail>

Behavior

The thumbnail root resolves an image for the current time, owns src and srcset on it, and reports its loading lifecycle.

The <img> child is optional: leave it out and <media-thumbnail> draws an image of its own in its shadow DOM; supply one to replace it and compose overlays or loading indicators beside it. Any crossorigin, loading, or fetchpriority the supplied image already carries is yours and wins over the element’s own attributes; the rest are filled in.

Supported source formats:

  • Text track: <track kind="metadata" label="thumbnails" src="...vtt">
  • JSON array: { url, startTime, endTime? }[]
  • JSON sprite array: { url, startTime, endTime?, width, height, coords }[]

Text-track mode needs an enclosing player element because it reads track state from the player store. JSON modes (thumbnails property) work without a player.

The component picks the latest thumbnail whose startTime is less than or equal to the current time, then scales/clips sprite tiles to fill CSS min/max constraints while preserving aspect ratio. Tiles scale up as well as down, so a preview whose max-width grows — a container query widening it in fullscreen, say — grows with it.

Cross-origin images

Leave crossOrigin unset and the component follows the media element. A cross-origin thumbnail <track> only loads when the media is CORS-enabled, so the images its cues point at are fetched with that same mode. Skins get this for free, with nothing to thread through.

Opt out to fetch them without CORS:

const thumbnail = document.querySelector('media-thumbnail');
thumbnail.crossOrigin = null;

Leaving the crossorigin attribute off is not the opt out. An absent attribute means “follow the media element”; only the property set to null opts out.

An empty value does not opt out either. The CORS settings attribute reads anything other than use-credentials as Anonymous, so it is a value like any other.

Thumbnails you supply through thumbnails never inherit, since they need not be related to the media element at all. Set crossOrigin yourself when those images need it.

Styling

Use state data attributes for pure CSS styling:

media-thumbnail[data-hidden] {
  display: none;
}

media-thumbnail[data-loading] {
  opacity: 0.6;
}

media-thumbnail[data-error] {
  outline: 1px solid #ef4444;
}

The image the element draws is exposed as a part. An <img> you supply is an ordinary child, so style it as a descendant:

media-thumbnail::part(image),
media-thumbnail > img {
  border-radius: 4px;
}

The element clips to the selected tile while the image inside spans the whole sprite sheet, so anything after the image in flow lands past the clip edge. Position the element and lay overlays over it:

media-thumbnail {
  position: relative;
}

media-thumbnail > .overlay {
  position: absolute;
  inset: 0;
}

Accessibility

<media-thumbnail> is decorative by default (aria-hidden="true"). It is intended for visual preview UX (for example, timeline hover previews) rather than primary accessible content.

Examples

Text Track (VTT)

<section class="demo">
  <video-player>
    <media-container>
      <video class="media" src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" preload="auto" muted playsinline crossorigin="anonymous">
        <track kind="metadata" label="thumbnails" src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt" default />
      </video>
      <media-thumbnail class="media-thumbnail" time="12"></media-thumbnail>
    </media-container>
  </video-player>
</section>

JSON Array

<media-thumbnail class="html-thumbnail-json" time="12" style="max-width: 240px"></media-thumbnail>

JSON Sprite Array

<media-thumbnail class="html-thumbnail-json-sprite" time="12" style="max-width: 240px"></media-thumbnail>

API Reference

media-thumbnail

<media-thumbnail> — resolves and sizes a time-based thumbnail into an image.

The element owns src and srcset on the active image. Left empty, it draws an image of its own in its shadow root. Supply an <img> child instead — <media-thumbnail time="12"><img alt=""></media-thumbnail> — to compose overlays or loading indicators beside the image the element controls. Any crossorigin, loading, or fetchpriority the child already carries wins over the element's own; the rest are filled in. Inside a skin, an <img slot="thumbnail"> of yours replaces the one the skin carries.

Props

PropTypeDefaultDetails
thumbnailsThumbnailImage[]—
timenumber—

State

State is reflected as data attributes for CSS styling.

PropertyTypeDetails
loadingboolean
errorboolean
hiddenboolean

Data attributes

AttributeDescription
data-loading
data-error
data-hidden