# Show timeline thumbnail previews

Preview frames along the timeline while the user scrubs, powered by a storyboard track.

Show preview frames along the timeline while the user scrubs, like YouTube’s seek previews.

> **Tip: Using Mux?**
>
> With [`<mux-video>`](https://videojs.org/docs/framework/html/reference/components/mux-video), the storyboard track is wired up for you — skip adding the track and go straight to the slider thumbnail. For other services, generate the storyboard on their platform and add the track yourself.

> **Note**
>
> Using a pre-built [skin](https://videojs.org/docs/framework/html/guides/skins)? It already includes the controls shown here. You may still need the media or player setup in this guide. The component examples are for building your own player UI from individual [components](https://videojs.org/docs/framework/html/guides/ui-components).

## Recommended approach

Add a `kind="metadata"` track labeled `thumbnails` whose cues reference storyboard images, then put a slider thumbnail inside the [`<media-time-slider>`](https://videojs.org/docs/framework/html/reference/components/time-slider). Scrubbing shows the frame under the pointer. The slider thumbnail reads the pointer time from the slider, so you never pass `time`; it otherwise accepts the same attributes as the standalone [`<media-thumbnail>`](https://videojs.org/docs/framework/html/reference/components/thumbnail).

`<media-slider-thumbnail>` draws its own image unless you supply an `<img>` child; set `crossorigin`, `loading`, or `fetchpriority` on the element itself, or on the image you supply. Inside a packaged skin, an `<img slot="thumbnail">` replaces the image the skin carries, so those attributes go on it:

```html
<video-player>
  <video-skin>
    <video src="video.mp4" crossorigin="anonymous">
      <track kind="metadata" label="thumbnails" src="storyboard.vtt" default />
    </video>
    <img slot="thumbnail" alt="" loading="lazy" fetchpriority="low" />
  </video-skin>
</video-player>
```

**index.html**

```html
<video-player class="video-player">
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop crossorigin="anonymous">
      <track kind="metadata" label="thumbnails" src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt" default />
    </video>
    <media-time-slider class="media-time-slider">
      <media-slider-track class="media-slider-track">
        <media-slider-fill class="media-slider-fill"></media-slider-fill>
      </media-slider-track>
      <media-slider-thumbnail class="media-slider-thumbnail"></media-slider-thumbnail>
    </media-time-slider>
  </media-container>
</video-player>
```

**index.css**

```css
.video-player {
  position: relative;
  display: block;
}

.video-player video {
  width: 100%;
}

.media-time-slider {
  position: absolute;
  right: 10px;
  bottom: 6px;
  left: 10px;
  display: flex;
  align-items: center;
  height: 20px;
  cursor: pointer;
}

.media-slider-track {
  position: absolute;
  right: 0;
  left: 0;
  height: 4px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  transition: height 150ms ease;
}

.media-time-slider[data-interactive] .media-slider-track {
  height: 6px;
}

.media-slider-fill {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--media-slider-fill);
  height: 100%;
  background: white;
  border-radius: 9999px;
}

/* Reveal the preview over the pointer while it is on the slider. */
.media-slider-thumbnail {
  position: absolute;
  bottom: 100%;
  left: var(--media-slider-pointer);
  max-width: 160px;
  margin-bottom: 8px;
  pointer-events: none;
  border-radius: 4px;
  opacity: 0;
  transform: translateX(-50%);
  transition: opacity 150ms ease;
}

.media-time-slider[data-pointing] .media-slider-thumbnail {
  opacity: 1;
}

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

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/time-slider';
import '@videojs/html/ui/slider-track';
import '@videojs/html/ui/slider-fill';
import '@videojs/html/ui/slider-thumbnail';
```

## How it works

- Previews come from a storyboard: a `kind="metadata"` text track labeled `thumbnails` whose WebVTT cues map time ranges to image URLs (optionally with sprite coordinates). The [text track feature](https://videojs.org/docs/framework/html/reference/api/feature-text-tracks) exposes these as `thumbnailCues` and `thumbnailTrackSrc`.
- The slider thumbnail renders the storyboard frame for the slider’s pointer position. While the pointer is over the slider, the slider sets `data-pointing` and the `--media-slider-pointer` CSS variable; the example’s CSS uses them to reveal the preview and position it over the pointer.
- To render a storyboard frame outside the slider — a fixed preview for a chapter list, for example — use the standalone [`<media-thumbnail>`](https://videojs.org/docs/framework/html/reference/components/thumbnail) element with an explicit `time`.

Each cue’s text is an image URL. An optional `#xywh=x,y,w,h` fragment crops one frame out of a sprite sheet:

**storyboard.vtt**

```txt
WEBVTT

00:00:00.000 --> 00:00:05.000
storyboard.jpg#xywh=0,0,256,160

00:00:05.000 --> 00:00:10.000
storyboard.jpg#xywh=256,0,256,160
```

## Availability and constraints

- Thumbnails require a storyboard asset generated alongside the media; the player doesn’t extract frames from the video itself.
- Cross-origin storyboards need CORS headers, and `crossorigin` on the media element for the track to load.
- Cues load asynchronously. The track appears before its cues are parsed, so previews fill in when the track finishes loading.
- Storyboard images download on demand; oversized sprite sheets hurt seek-preview responsiveness more than they help quality.

## Troubleshooting

### Thumbnails don’t appear while scrubbing

Confirm the storyboard track is `kind="metadata"` with `label="thumbnails"` and has the `default` attribute — the browser leaves a non-default track disabled and never loads its cues. Then confirm the VTT loads (network panel) and cross-origin assets have CORS headers with `crossorigin` set on the media element.

### Thumbnails are blurry or slow

Regenerate the storyboard with a larger frame size, or split giant sprite sheets into smaller images.

## Related pages

### Components

- [media-thumbnail](https://videojs.org/docs/framework/html/reference/components/thumbnail): Time-based thumbnail preview component for timeline scrubbing and hover previews
- [media-slider](https://videojs.org/docs/framework/html/reference/components/slider): A composable slider component with track, fill, thumb, preview, and value parts
- [media-time-slider](https://videojs.org/docs/framework/html/reference/components/time-slider): A slider component for seeking through media playback time

### API

- [Text tracks](https://videojs.org/docs/framework/html/reference/api/feature-text-tracks): Subtitles, captions, and chapter track state for the player store

### Guides

- [Add a poster and loading placeholder](https://videojs.org/docs/framework/html/guides/poster): Set the image shown before playback, then add a lightweight placeholder while it loads
- [Show captions and subtitles](https://videojs.org/docs/framework/html/guides/captions): Show captions and subtitles, and let users turn them on and pick a language.

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
