# 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 [MuxVideo](https://videojs.org/docs/framework/react/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/react/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/react/guides/ui-components).

## Recommended approach

Add a `kind="metadata"` track labeled `thumbnails` whose cues reference storyboard images, then put a slider thumbnail inside the [TimeSlider](https://videojs.org/docs/framework/react/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 props as the standalone [Thumbnail](https://videojs.org/docs/framework/react/reference/components/thumbnail).

Compose `Slider.Thumbnail.Root` around `Slider.Thumbnail.Image`, and put image settings such as `crossOrigin`, `loading`, and `fetchPriority` on the image part. Inside a packaged skin, pass `renderThumbnail` to draw the image yourself; the skin still supplies `src` and sizing:

```tsx
<VideoSkin renderThumbnail={<img alt="" loading="lazy" fetchPriority="low" />} />
```

**App.tsx**

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

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

export default function BasicUsage() {
  return (
    <Player>
      <Container className="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>
        <TimeSlider.Root className="media-time-slider">
          <TimeSlider.Track className="media-slider-track">
            <TimeSlider.Fill className="media-slider-fill" />
          </TimeSlider.Track>
          <Slider.Thumbnail.Root className="media-slider-thumbnail">
            <Slider.Thumbnail.Image className="media-slider-thumbnail-image" />
          </Slider.Thumbnail.Root>
        </TimeSlider.Root>
      </Container>
    </Player>
  );
}
```

**App.css**

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

.media-container 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;
}

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

## 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/react/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 [Thumbnail](https://videojs.org/docs/framework/react/reference/components/thumbnail) component 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

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

### API

- [Text tracks](https://videojs.org/docs/framework/react/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/react/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/react/guides/captions): Show captions and subtitles, and let users turn them on and pick a language.

---

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