# Show captions and subtitles

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

> **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 WebVTT tracks as `<track>` children of the media element, and give users a [CaptionsButton](https://videojs.org/docs/framework/react/reference/components/captions-button) to toggle them.

**App.tsx**

```tsx
import { CaptionsButton, Container } from '@videojs/react';
import { Video, VideoPlayer } from '@videojs/react/video';

export default function BasicUsage() {
  return (
    <VideoPlayer>
      <Container className="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>
        <CaptionsButton
          className="media-captions-button"
          render={(props, state) => (
            <button {...props}>{state.subtitlesShowing ? 'Captions Off' : 'Captions On'}</button>
          )}
        />
      </Container>
    </VideoPlayer>
  );
}
```

**App.css**

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

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

## How it works

The [text track feature](https://videojs.org/docs/framework/react/reference/api/feature-text-tracks) 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.

`useCaptionsOptions` provides the option list (including “Off”), the selected value, and availability:

```tsx
import { Menu, useCaptionsOptions } from '@videojs/react';

function CaptionsMenu() {
  const captions = useCaptionsOptions();
  if (captions?.state.availability !== 'available') return null;

  return (
    <Menu.Root side="top" align="end">
      <Menu.Trigger render={<button type="button" />}>Captions</Menu.Trigger>
      <Menu.Content>
        <Menu.RadioGroup value={captions.value} onValueChange={captions.setValue} aria-label="Captions">
          {captions.options.map((option) => (
            <Menu.RadioItem key={option.value} value={option.value} disabled={option.disabled}>
              {option.label}
            </Menu.RadioItem>
          ))}
        </Menu.RadioGroup>
      </Menu.Content>
    </Menu.Root>
  );
}
```

See the [CaptionsRadioGroup](https://videojs.org/docs/framework/react/reference/components/captions-radio-group) reference for the complete pattern.

## 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 CaptionsButton 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

- [CaptionsButton](https://videojs.org/docs/framework/react/reference/components/captions-button): Accessible captions toggle button with availability detection and state reflection
- [CaptionsRadioGroup](https://videojs.org/docs/framework/react/reference/components/captions-radio-group): A menu radio group for selecting caption and subtitle tracks
- [Menu](https://videojs.org/docs/framework/react/reference/components/menu): A composable menu component for settings, option selection, and actions

### API

- [Text tracks](https://videojs.org/docs/framework/react/reference/api/feature-text-tracks): Subtitles, captions, and chapter track state for the player store
- [useCaptionsOptions](https://videojs.org/docs/framework/react/reference/api/use-captions-options): Hook to build captions menu options from the player text track state

### Guides

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

---

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