# 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](https://www.mux.com?utm_source=videojs&utm_campaign=vjs10) 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:

```tsx
<Video src="video.mp4" crossOrigin="anonymous">
  <track
    kind="metadata"
    label="thumbnails"
    src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt"
    default
  />
</Video>
<Thumbnail.Root time={12}>
  <Thumbnail.Image />
</Thumbnail.Root>
```

A same-origin track needs none of this.

## Import

```tsx
import { Thumbnail } from '@videojs/react';
```

## Anatomy

```tsx
<Thumbnail.Root>
  <Thumbnail.Image />
</Thumbnail.Root>
```

## Behavior

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

Render that image with `Thumbnail.Image`.

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 `Player` because it reads track state from the player store. JSON modes (`thumbnails` prop) 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:

```tsx
<Thumbnail.Root time={12}>
  <Thumbnail.Image crossOrigin={null} />
</Thumbnail.Root>
```

An empty value does not opt out either. The [CORS settings attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) 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:

React renders a root `<div>` around an `<img>`. State attributes belong to `Thumbnail.Root`, while image attributes and `render` belong to `Thumbnail.Image`:

```tsx
<Thumbnail.Root className="media-thumbnail" time={12}>
  <Thumbnail.Image className="media-thumbnail-image" loading="eager" />
  <div className="media-thumbnail-overlay" aria-hidden />
</Thumbnail.Root>
```

The root 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 root and lay overlays over it:

```css
.media-thumbnail {
  position: relative;
}

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

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

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

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

## Accessibility

`Thumbnail.Root` 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)

**App.tsx**

```tsx
import { Container, createPlayer, Thumbnail } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function TextTrackUsage() {
  return (
    <Player>
      <Container className="demo">
        <Video
          className="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>
        <Thumbnail.Root className="media-thumbnail" time={12}>
          <Thumbnail.Image className="media-thumbnail-image" />
        </Thumbnail.Root>
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.demo {
  position: relative;
  max-width: 280px;
}

.media {
  position: absolute;
  width: 1px;
  height: 1px;
  pointer-events: none;
  opacity: 0;
}

.media-thumbnail {
  display: block;
  width: auto;
  min-width: 0;
  max-width: 240px;
}

.media-thumbnail-image {
  display: block;
}

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

### JSON Array

**App.tsx**

```tsx
import { Thumbnail } from '@videojs/react';

const THUMBNAILS = [
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=0',
    startTime: 0,
    endTime: 10,
  },
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=10',
    startTime: 10,
    endTime: 20,
  },
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=20',
    startTime: 20,
  },
];

export default function JsonUsage() {
  return (
    <Thumbnail.Root thumbnails={THUMBNAILS} time={12} style={{ maxWidth: 240 }}>
      <Thumbnail.Image />
    </Thumbnail.Root>
  );
}
```

### JSON Sprite Array

**App.tsx**

```tsx
import { Thumbnail } from '@videojs/react';

const THUMBNAILS = [
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
    startTime: 0,
    endTime: 10,
    width: 284,
    height: 160,
    coords: { x: 0, y: 0 },
  },
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
    startTime: 10,
    endTime: 20,
    width: 284,
    height: 160,
    coords: { x: 284, y: 0 },
  },
  {
    url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
    startTime: 20,
    width: 284,
    height: 160,
    coords: { x: 568, y: 0 },
  },
];

export default function JsonSpriteUsage() {
  return (
    <Thumbnail.Root thumbnails={THUMBNAILS} time={12} style={{ maxWidth: 240 }}>
      <Thumbnail.Image />
    </Thumbnail.Root>
  );
}
```

## API Reference

### Root

Resolves, sizes, and clips a thumbnail for a point in time.

Renders a `div` and exposes `data-hidden`, `data-loading`, and `data-error` for styling every layer in the preview. Render `Thumbnail.Image` inside it for the image the root controls and measures.

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `thumbnails` | `ThumbnailImage[]` | — | Pre-parsed thumbnail images — bypasses the automatic `<track>` detection. |
| `time` | `number` | — | Time in seconds to display the thumbnail for. |

#### State

State is accessible via the `render`, `className`, and `style` props.

| Property | Type | Description |
| --- | --- | --- |
| `loading` | `boolean` | The thumbnail image is loading. |
| `error` | `boolean` | The thumbnail image failed to load. |
| `hidden` | `boolean` | Whether the component is hidden because no thumbnail is available and it is not loading. |

#### Data attributes

| Attribute | Description |
| --- | --- |
| `data-loading` | |
| `data-error` | |
| `data-hidden` | |

### Image

Displays the image selected and measured by `Thumbnail.Root`.

Renders an `img`, so native image attributes and the `render` escape hatch remain available without replacing the root that owns thumbnail state.

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: ThumbnailState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `crossOrigin` | `ThumbnailCrossOrigin` | — | CORS setting for the selected image. Leave unset to follow the media element, or pass `null` to opt out. |
| `fetchPriority` | `ThumbnailFetchPriority` | — | Image fetch priority hint. |
| `loading` | `ThumbnailLoading` | — | Image loading strategy. |
| `render` | `ReactElement \| ((props: HTMLProps, state: ThumbnailState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: ThumbnailState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
