media-captions-radio-group
A menu radio group for selecting caption and subtitle tracks
Creates radio items from the player text track state and selects the showing caption or subtitle track.
Import
import { CaptionsRadioGroup } from '@videojs/react/ui/captions-radio-group';import '@videojs/html/ui/captions-radio-group';Anatomy
<CaptionsRadioGroup.Root>
<Menu.Trigger>
<CaptionsRadioGroup.Value />
</Menu.Trigger>
<Menu.Content>
<CaptionsRadioGroup.Options
renderItem={(props, item) => (
<Menu.RadioItem {...props}>
{item.label}
<Menu.ItemIndicator checked={item.checked} />
</Menu.RadioItem>
)}
/>
</Menu.Content>
</CaptionsRadioGroup.Root><media-captions-radio-group>
<template>
<media-menu-radio-item>
<span data-part="label"></span>
<media-menu-item-indicator></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-captions-radio-group>Behavior
The group is available when the configured media exposes at least one caption or subtitle track. The first generated option is Off; selecting it hides captions. Track labels use the track label, then language, then kind. Pass formatTrack to customize the visible labels.
CaptionsRadioGroup.Root uses useCaptionsOptions to own selection and renders no element. Wrap the menu’s Menu.Trigger and Menu.Content in it so the trigger inherits the group’s disabled and hidden state and exposes data-availability. CaptionsRadioGroup.Options renders the items, including Off: its required renderItem callback renders the Menu.RadioItem root and receives item state containing the translated label and current checked value. CaptionsRadioGroup.Value displays the selected label, typically inside the trigger. Options and Value render null when the text tracks feature is not configured.
The CaptionsRadioGroup export from the @videojs/react root is a compatibility component that composes Root and Options behind the former single-component props. Prefer the parts for new code.
<media-captions-radio-group> generates <media-menu-radio-item> children, including the Off item. Add an optional <template> with a single <media-menu-radio-item> to customize each generated item.
Styling
| Attribute | Values | Description |
|---|---|---|
data-active |
Present / absent | Present when captions are enabled. |
data-disabled |
Present / absent | Present when track selection is disabled. |
data-hidden |
Present / absent | Present when caption or subtitle tracks are unavailable. |
data-availability |
"available" / "unavailable" |
Whether caption or subtitle tracks are available. |
Unavailable groups receive the native hidden attribute.
Accessibility
The group uses the menu radio group pattern.
CaptionsRadioGroup.Options receives its accessible label from the label prop on CaptionsRadioGroup.Root or defaults to Captions. Override it with aria-label or aria-labelledby on CaptionsRadioGroup.Options.
The element receives an accessible label from the label property or defaults to Captions.
Examples
Basic usage
import { Container, createPlayer, Menu } from '@videojs/react';
import { CaptionsRadioGroup } from '@videojs/react/ui/captions-radio-group';
import { Video, videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
function CaptionsMenu(): ReactNode {
return (
<Menu.Root side="top" align="end">
<CaptionsRadioGroup.Root>
<Menu.Trigger className="settings-trigger" render={<button type="button" />}>
Captions
<CaptionsRadioGroup.Value className="menu-hint" />
</Menu.Trigger>
<Menu.Popup className="menu">
<Menu.Content>
<CaptionsRadioGroup.Options
className="menu-group"
renderItem={(props, item) => (
<Menu.RadioItem {...props} className="menu-item">
{item.label}
<Menu.ItemIndicator checked={item.checked} forceMount className="menu-indicator">
✓
</Menu.ItemIndicator>
</Menu.RadioItem>
)}
/>
</Menu.Content>
</Menu.Popup>
</CaptionsRadioGroup.Root>
</Menu.Root>
);
}
export default function BasicUsage() {
return (
<Player>
<Container className="media-container">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop>
<track kind="captions" src="/docs/demos/captions-button/captions.vtt" srcLang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srcLang="es" label="Spanish" />
</Video>
<div className="menu-bar">
<CaptionsMenu />
</div>
</Container>
</Player>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu-hint {
margin-left: 8px;
color: rgba(0, 0, 0, 0.6);
}
.menu-hint:empty {
display: none;
}
.menu {
--media-menu-side-offset: 8px;
box-sizing: border-box;
display: grid;
gap: 2px;
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
margin: 0;
overflow: auto;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border: 0;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-item {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
.menu-indicator {
opacity: 0;
}
[role="menuitemradio"][aria-checked="true"] .menu-indicator {
opacity: 1;
}
<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/captions-button/captions.vtt" srclang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srclang="es" label="Spanish" />
</video>
<div class="menu-bar">
<button type="button" commandfor="captions-menu" class="settings-trigger">Captions</button>
<media-menu id="captions-menu" side="top" align="end" class="menu">
<media-menu-content>
<media-captions-radio-group class="menu-group" label="Captions">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator">✓</media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-captions-radio-group>
</media-menu-content>
</media-menu>
</div>
</media-container>
</video-player>
.video-player media-container {
position: relative;
display: block;
}
.video-player media-container video {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu {
--media-menu-side-offset: 8px;
box-sizing: border-box;
display: grid;
gap: 2px;
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
overflow: auto;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-item {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
.menu-indicator {
opacity: 0;
}
media-menu-radio-item[aria-checked="true"] .menu-indicator {
opacity: 1;
}
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/menu';
import '@videojs/html/ui/captions-radio-group';
API Reference
Rootmedia-captions-radio-group
Owns captions option state and shares it with an enclosing menu. Does not render a DOM element.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
disabled | boolean | false | |
| |||
formatTrack | string) | function | formatTrackLabel | |
| |||
label | object | '' | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
subtitlesShowing | boolean | |
| ||
label | { key: string; text: string } | string | |
value | string | |
| ||
options | readonly Option[] | |
| ||
disabled | boolean | |
| ||
availability | 'available' | 'unavailable' | |
| ||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-active | ||
| ||
data-disabled | ||
| ||
data-availability | 'available' | 'unavailable' | |
| ||
Events
| Event | Description |
|---|---|
value-change | Fired when the selected value changes. |
OptionsOptions
Renders menu radio items for the player's captions and subtitles tracks.
ValueValue
Displays the selected captions label.