# Show captions and subtitles

Show captions and subtitles, and let users turn them on and pick a language.

> **Note**
>
> Using a pre-built [skin](./skins.md)? 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](./ui-components.md).

## Recommended approach

Add WebVTT tracks as `<track>` children of the media element, and give users a [`<media-captions-button>`](../reference/components/captions-button.md) to toggle them.

**index.html**

```html
<video-player class="video-player">
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop>
      <track kind="captions" src="/docs/demos/text-tracks/captions.vtt" srclang="en" label="English" default />
    </video>
    <media-captions-button class="media-captions-button">
      <span class="active">Captions Off</span>
      <span class="inactive">Captions On</span>
    </media-captions-button>
  </media-container>
</video-player>
```

**index.css**

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

.video-player video {
  width: 100%;
  aspect-ratio: 16 / 9;
}

.media-captions-button {
  position: absolute;
  bottom: 10px;
  left: 10px;
  padding-block: 8px;
  padding-inline: 20px;
  color: black;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}

.media-captions-button .active {
  display: none;
}
.media-captions-button .inactive {
  display: none;
}
.media-captions-button[data-active] .active {
  display: inline;
}
.media-captions-button:not([data-active]) .inactive {
  display: inline;
}
```

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/captions-button';
```

## How it works

The [text track feature](../reference/api/feature-text-tracks.md) mirrors the media element’s `textTracks` into player state:

- `textTrackList` holds every track with its `id`, `kind`, `label`, `language`, and `mode`.
- `subtitlesShowing` is `true` when any caption or subtitle track is showing.
- `toggleSubtitles(forceShow?)` shows or hides all caption and subtitle tracks.
- `selectSubtitlesTrack(id)` shows one track and disables the rest; pass `'off'` to disable all.

Tracks don’t have to come from `<track>` elements: tracks that streaming media exposes (for example in-manifest HLS captions) appear in `textTrackList` the same way.

The browser renders the cues. Style them with the `::cue` pseudo-element.

## Availability and constraints

- Captions availability is `'unavailable'` until the media has at least one caption or subtitle track; caption controls render nothing in that case.
- Cues load asynchronously. A track appears in `textTrackList` before its cues are parsed, so cue-driven UI fills in when the track finishes loading.
- Cross-origin track files require CORS: serve the VTT with CORS headers and set `crossorigin` on the media element.
- Track selection UIs identify tracks by `label` and `language`; give every track both.
- `default` on a `<track>` makes the browser show it initially.

## Common variations

### Language selection menu

For multiple languages, render a menu instead of a toggle.

Use [`<media-captions-radio-group>`](../reference/components/captions-radio-group.md) inside a menu or popover for explicit track selection.

## Troubleshooting

### Captions don’t appear

Confirm that:

- The track `kind` is `captions` or `subtitles`.
- The VTT file loads (check the network panel for the track request).
- The track is enabled: `default` on the track element, a `<media-captions-button>` toggle, or `toggleSubtitles(true)`.

### Captions work locally but not in production

The track file is served from another origin without CORS headers. Serve it with `Access-Control-Allow-Origin` and set `crossorigin` on the media element.

### The selection menu is empty

The media has no caption or subtitle tracks, so captions availability is `'unavailable'`. For streaming sources, confirm the manifest actually declares text tracks.

## Related pages

### Components

- [media-captions-button](../reference/components/captions-button.md): Accessible captions toggle button with availability detection and state reflection
- [media-captions-radio-group](../reference/components/captions-radio-group.md): A menu radio group for selecting caption and subtitle tracks
- [media-menu](../reference/components/menu.md): A composable menu component for settings, option selection, and actions

### API

- [Text tracks](../reference/api/feature-text-tracks.md): Subtitles, captions, and chapter track state for the player store

### Guides

- [Show timeline thumbnail previews](./thumbnails.md): Preview frames along the timeline while the user scrubs, powered by a storyboard track.
- [Media sources](./media-sources.md): Set what a media element plays and how its engine plays it with the structured source property
- [Accessibility](./accessibility.md): How Video.js approaches accessibility, and what you should consider if you're deeply customizing your player