Thumbnail
Time-based thumbnail preview component for timeline scrubbing and hover previews
Quick Start: Video Track
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
<Video src="video.mp4">
<track
kind="metadata"
label="thumbnails"
src="https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.vtt"
default
/>
</Video>
<Thumbnail time={12} /><video src="video.mp4">
<track
kind="metadata"
label="thumbnails"
src="https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.vtt"
default
/>
</video>
<media-thumbnail time="12"></media-thumbnail>Anatomy
<Thumbnail time={12} /><media-thumbnail time="12"></media-thumbnail>Behavior
Thumbnail resolves an image for the current time.
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 }[]
In React, text-track mode needs Player.Provider because it reads track state from the player store. JSON modes (thumbnails prop) work without Provider.
The component picks the latest thumbnail whose startTime is less than or equal to the current time, then scales/clips sprite tiles to fit CSS min/max constraints while preserving aspect ratio.
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;
}Accessibility
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)
import { createPlayer, Thumbnail } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const Player = createPlayer({ features: videoFeatures });
export default function TextTrackUsage() {
return (
<Player.Provider>
<Player.Container className="react-thumbnail-text-track">
<Video
className="react-thumbnail-text-track__media"
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
preload="auto"
muted
playsInline
crossOrigin="anonymous"
>
<track kind="metadata" label="thumbnails" src="/docs/demos/thumbnail/basic.vtt" default />
</Video>
<Thumbnail className="react-thumbnail-text-track__thumbnail" time={12} />
</Player.Container>
</Player.Provider>
);
}
.react-thumbnail-text-track {
position: relative;
max-width: 280px;
}
.react-thumbnail-text-track__media {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.react-thumbnail-text-track__thumbnail {
display: block;
width: auto;
min-width: 0;
max-width: 240px;
}
.react-thumbnail-text-track__thumbnail[data-hidden] {
display: none;
}
<section class="html-thumbnail-text-track">
<video-player>
<media-container>
<video
class="html-thumbnail-text-track__media"
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4"
preload="auto"
muted
playsinline
crossorigin="anonymous"
>
<track
kind="metadata"
label="thumbnails"
src="/docs/demos/thumbnail/basic.vtt"
default
/>
</video>
<media-thumbnail class="html-thumbnail-text-track__thumbnail" time="12"></media-thumbnail>
</media-container>
</video-player>
</section>
.html-thumbnail-text-track {
position: relative;
display: block;
max-width: 280px;
}
.html-thumbnail-text-track__media {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.html-thumbnail-text-track__thumbnail {
display: block;
width: auto;
min-width: 0;
max-width: 240px;
}
.html-thumbnail-text-track__thumbnail[data-hidden] {
display: none;
}
import '@videojs/html/video/player';
import '@videojs/html/ui/thumbnail';
JSON Array
import { Thumbnail } from '@videojs/react';
const THUMBNAILS = [
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=0',
startTime: 0,
endTime: 10,
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=10',
startTime: 10,
endTime: 20,
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=20',
startTime: 20,
},
];
export default function JsonUsage() {
return <Thumbnail thumbnails={THUMBNAILS} time={12} style={{ maxWidth: 240 }} />;
}
<media-thumbnail time="12" style="max-width: 240px;"></media-thumbnail>
import '@videojs/html/ui/thumbnail';
type DemoThumbnailImage = {
url: string;
startTime: number;
endTime?: number;
};
type ThumbnailDemoElement = HTMLElement & { thumbnails?: DemoThumbnailImage[] };
const thumbnail = document.querySelector<ThumbnailDemoElement>('media-thumbnail');
if (thumbnail) {
thumbnail.thumbnails = [
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=0',
startTime: 0,
endTime: 10,
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=10',
startTime: 10,
endTime: 20,
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/thumbnail.jpg?time=20',
startTime: 20,
},
];
}
JSON Sprite Array
import { Thumbnail } from '@videojs/react';
const THUMBNAILS = [
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 0,
endTime: 10,
width: 284,
height: 160,
coords: { x: 0, y: 0 },
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 10,
endTime: 20,
width: 284,
height: 160,
coords: { x: 284, y: 0 },
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 20,
width: 284,
height: 160,
coords: { x: 568, y: 0 },
},
];
export default function JsonSpriteUsage() {
return <Thumbnail thumbnails={THUMBNAILS} time={12} style={{ maxWidth: 240 }} />;
}
<media-thumbnail time="12" style="max-width: 240px;"></media-thumbnail>
import '@videojs/html/ui/thumbnail';
type DemoThumbnailImage = {
url: string;
startTime: number;
endTime?: number;
width?: number;
height?: number;
coords?: { x: number; y: number };
};
type ThumbnailDemoElement = HTMLElement & { thumbnails?: DemoThumbnailImage[] };
const thumbnail = document.querySelector<ThumbnailDemoElement>('media-thumbnail');
if (thumbnail) {
thumbnail.thumbnails = [
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 0,
endTime: 10,
width: 284,
height: 160,
coords: { x: 0, y: 0 },
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 10,
endTime: 20,
width: 284,
height: 160,
coords: { x: 284, y: 0 },
},
{
url: 'https://image.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/storyboard.jpg',
startTime: 20,
width: 284,
height: 160,
coords: { x: 568, y: 0 },
},
];
}
API Reference
Props
| Prop | Type | Default | |
|---|---|---|---|
crossOrigin | 'anonymous' | 'use-credentials' | '' ... | — | |
| |||
fetchPriority | 'high' | 'low' | 'auto' | — | |
| |||
loading | 'eager' | 'lazy' | — | |
| |||
time | number | — | |
| |||
State
render, className, and style props.
| Property | Type | |
|---|---|---|
loading | boolean | |
| ||
error | boolean | |
| ||
Data attributes
| Attribute | Type | |
|---|---|---|
data-loading | ||
data-error |
