`. Add a `className` to select it:
```css
.volume-popup[data-open] {
opacity: 1;
}
```
## Accessibility
The trigger receives `aria-haspopup`, `aria-expanded`, and `aria-controls` while the volume popup is available. When only mute is available, the trigger keeps the mute button’s own accessible name and behavior without popup ARIA.
## Examples
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, MuteButton, VolumePopover, VolumeSlider } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
{state.muted ? 'Unmute' : 'Mute'} } />
}
/>
);
}
```
**App.css**
```css
.react-volume-popover-basic {
position: relative;
}
.react-volume-popover-basic video {
width: 100%;
}
.react-volume-popover-basic__controls {
position: absolute;
bottom: 10px;
left: 10px;
}
.react-volume-popover-basic__trigger {
padding: 8px 20px;
color: black;
cursor: pointer;
background: rgb(255 255 255 / 70%);
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.react-volume-popover-basic__popup {
--media-popover-side-offset: 8px;
width: 40px;
height: 120px;
padding: 12px;
margin: 0;
background: rgb(0 0 0 / 85%);
border: 0;
border-radius: 8px;
}
.react-volume-popover-basic__slider {
position: relative;
width: 16px;
height: 96px;
margin: auto;
cursor: pointer;
}
.react-volume-popover-basic__track {
position: absolute;
top: 0;
bottom: 0;
left: 6px;
width: 4px;
background: rgb(255 255 255 / 30%);
border-radius: 9999px;
}
.react-volume-popover-basic__fill {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: var(--media-slider-fill);
background: white;
border-radius: inherit;
}
.react-volume-popover-basic__thumb {
position: absolute;
bottom: var(--media-slider-fill);
left: 1px;
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
transform: translateY(50%);
}
```
## API Reference
### Root
Owns the popover interaction lifecycle and provides volume availability to the parts.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `'start' \| 'center' \| 'end'` | — | Alignment of the popup along the trigger's edge. |
| `boundary` | `'viewport' \| 'container' \| string & {}` | — | Boundary used to constrain the popup position. |
| `closeDelay` | `number` | — | Delay in ms before closing after pointer leaves. |
| `closeOnEscape` | `boolean` | — | Close the popup when the Escape key is pressed. |
| `closeOnOutsideClick` | `boolean` | — | Close the popup when clicking outside the trigger and popup. |
| `defaultOpen` | `boolean` | — | Initial open state for uncontrolled usage. |
| `delay` | `number` | — | Delay in ms before opening on hover. |
| `modal` | `boolean \| 'trap-focus'` | — | - `false` (default): non-modal; background content remains interactive.
- `true`: modal; sets `aria-modal="true"` on the popup.
- `'trap-focus'`: reserved for future focus-trapping behavior. |
| `open` | `boolean` | — | Controlled open state. When set, the consumer is responsible for toggling. |
| `openOnHover` | `boolean` | — | Open the popup on pointer hover instead of click. |
| `side` | `'top' \| 'bottom' \| 'left' \| 'right'` | — | Preferred side of the trigger for the popup. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | |
| `status` | `'idle' \| 'starting' \| 'ending'` | |
| `side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Preferred side of the trigger for the popup. |
| `align` | `'start' \| 'center' \| 'end'` | |
| `modal` | `boolean \| 'trap-focus'` | |
| `availability` | `'available' \| 'unavailable' \| 'unsupported'` | Whether volume level controls are available. |
| `hidden` | `boolean` | Whether the popup is hidden because volume level controls are unavailable. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the popover is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the popup is aligned relative to its side. |
| `data-starting-style` | — | Present during the open transition. |
| `data-ending-style` | — | Present during the close transition. |
| `data-availability` | `'available' \| 'unavailable' \| 'unsupported'` | Indicates volume control availability (`available`, `unavailable`, or `unsupported`). |
| `data-hidden` | — | Present when volume level controls are unavailable. |
### Popup
Positioned volume content. Omitted when volume level controls are unavailable.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: PopoverState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: PopoverState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: PopoverState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Trigger
Opens the volume popup, or renders its mute-button fallback when volume level controls are unavailable.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: PopoverState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: PopoverState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: PopoverState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# AudioTrackRadioGroup
A menu radio group for selecting an audio track
Creates radio items from the player audio track state and selects the enabled track.
## Import
```tsx
import { AudioTrackRadioGroup } from '@videojs/react';
```
## Anatomy
```tsx
(
{item.label}
)}
/>
```
## Behavior
The group is available when the configured media exposes more than one audio track. Labels use the track label, then language, then kind. Pass `formatTrack` to customize the visible labels.
`AudioTrackRadioGroup.Root` uses [`useAudioTrackOptions`](https://videojs.org/docs/framework/react/reference/api/use-audio-track-options) 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`. `AudioTrackRadioGroup.Options` renders the items: its required `renderItem` callback renders the [`Menu.RadioItem`](https://videojs.org/docs/framework/react/reference/components/menu) root and receives item state containing the translated `label` and current `checked` value. `AudioTrackRadioGroup.Value` displays the selected label, typically inside the trigger. `Options` and `Value` render `null` when the [audio track feature](https://videojs.org/docs/framework/react/reference/api/feature-audio-track) is not configured.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-audio-track` | `string` | Current audio track value. |
| `data-disabled` | Present / absent | Present when audio track selection is disabled. |
| `data-hidden` | Present / absent | Present when multiple audio tracks are unavailable. |
| `data-availability` | `"available"` / `"unavailable"` | Whether multiple audio tracks are available. |
Unavailable groups receive the native `hidden` attribute.
## Accessibility
The group uses the menu radio group pattern.
`AudioTrackRadioGroup.Options` receives its accessible label from the `label` prop on `AudioTrackRadioGroup.Root` or defaults to `Audio`. Override it with `aria-label` or `aria-labelledby` on `AudioTrackRadioGroup.Options`.
## Examples
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, Menu, AudioTrackRadioGroup } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/s41JYeqIpBMBzE4OzxDyGR2yrp2hD1CQ6gJN9SlVGDQ.m3u8';
function AudioMenu(): ReactNode {
return (
}>
Audio
(
{item.label}
✓
)}
/>
);
}
export default function BasicUsage() {
return (
);
}
```
**App.css**
```css
.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;
}
```
## API Reference
### Root
Owns audio-track option state and shares it with an enclosing menu. Does not render a DOM element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether audio track selection is disabled. |
| `formatTrack` | `((track: MediaAudioTrack) => Text \| string)` | `formatTrackLabel` | Custom formatter for visible track labels. |
| `label` | `{ key: string; text: string } \| string \| ((state: AudioTrackRadioGroupState) => Text \| string)` | `''` | Custom label for the options group. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `label` | `{ key: string; text: string } \| string` | |
| `value` | `string` | Current radio-group value. |
| `options` | `readonly Option[]` | Ordered options displayed by platform adapters. |
| `disabled` | `boolean` | Whether the entire option group is disabled. |
| `hidden` | `boolean` | Whether the option group is hidden because no meaningful selection is available. |
| `availability` | `'available' \| 'unavailable'` | Whether the media exposes a meaningful selection. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-audio-track` | `string` | Current audio track value. |
| `data-disabled` | — | Present when audio track selection is disabled. |
| `data-hidden` | — | Present when audio track selection is unavailable. |
| `data-availability` | `'available' \| 'unavailable'` | Indicates audio track availability (`available` or `unavailable`). |
### Options
Renders menu radio items for the player's available audio tracks.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: AudioTrackRadioGroupState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: AudioTrackRadioGroupState) => ReactElement \| null)` | — | Render prop for custom element. |
| `renderItem` | `((props: AudioTrackRadioGroupItemProps, state: AudioTrackRadioGroupItemState) => ReactElement)` | — | Render one consumer-owned menu radio item for every audio track. |
| `style` | `CSSProperties \| ((state: AudioTrackRadioGroupState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Value
Displays the selected audio-track label.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: AudioTrackOptionsResult) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: AudioTrackOptionsResult) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: AudioTrackOptionsResult) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# CaptionsRadioGroup
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
```tsx
import { CaptionsRadioGroup } from '@videojs/react';
```
## Anatomy
```tsx
(
{item.label}
)}
/>
```
## 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`](https://videojs.org/docs/framework/react/reference/api/use-captions-options) 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`](https://videojs.org/docs/framework/react/reference/components/menu) 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](https://videojs.org/docs/framework/react/reference/api/feature-text-tracks) is not configured.
## 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`.
## Examples
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, Menu, CaptionsRadioGroup } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
function CaptionsMenu(): ReactNode {
return (
}>
Captions
(
{item.label}
✓
)}
/>
);
}
export default function BasicUsage() {
return (
);
}
```
**App.css**
```css
.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;
}
```
## API Reference
### Root
Owns captions option state and shares it with an enclosing menu. Does not render a DOM element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether track selection is disabled. |
| `formatTrack` | `((track: MediaTextTrack) => Text \| string)` | `formatTrackLabel` | Custom formatter for visible track labels. |
| `label` | `{ key: string; text: string } \| string \| ((state: CaptionsRadioGroupState) => Text \| string)` | `''` | Custom label for the options group. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `subtitlesShowing` | `boolean` | Whether a captions/subtitles track is showing. |
| `label` | `{ key: string; text: string } \| string` | |
| `value` | `string` | Current radio-group value. |
| `options` | `readonly Option[]` | Ordered options displayed by platform adapters. |
| `disabled` | `boolean` | Whether the entire option group is disabled. |
| `hidden` | `boolean` | Whether the option group is hidden because no meaningful selection is available. |
| `availability` | `'available' \| 'unavailable'` | Whether the media exposes a meaningful selection. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-active` | — | Present when captions are enabled. |
| `data-disabled` | — | Present when track selection is disabled. |
| `data-hidden` | — | Present when track selection is unavailable. |
| `data-availability` | `'available' \| 'unavailable'` | Indicates captions availability (`available` or `unavailable`). |
### Options
Renders menu radio items for the player's captions and subtitles tracks.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: CaptionsRadioGroupState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: CaptionsRadioGroupState) => ReactElement \| null)` | — | Render prop for custom element. |
| `renderItem` | `((props: CaptionsRadioGroupItemProps, state: CaptionsRadioGroupItemState) => ReactElement)` | — | Render one consumer-owned menu radio item for every captions option. |
| `style` | `CSSProperties \| ((state: CaptionsRadioGroupState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Value
Displays the selected captions label.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: CaptionsOptionsResult) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: CaptionsOptionsResult) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: CaptionsOptionsResult) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# PlaybackRateRadioGroup
A menu radio group for selecting playback speed
Creates radio items from the player playback rate state and selects the playback speed.
## Import
```tsx
import { PlaybackRateRadioGroup } from '@videojs/react';
```
## Anatomy
```tsx
(
{item.label}
)}
/>
```
## Behavior
The group is available when the configured media exposes playback rates. Option labels default to the rate followed by a multiplication sign (e.g. `1.5×`); pass `formatRate` to customize them.
`PlaybackRateRadioGroup.Root` uses [`usePlaybackRateOptions`](https://videojs.org/docs/framework/react/reference/api/use-playback-rate-options) 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`. `PlaybackRateRadioGroup.Options` renders the items: its required `renderItem` callback renders the [`Menu.RadioItem`](https://videojs.org/docs/framework/react/reference/components/menu) root and receives item state containing the translated `label` and current `checked` value. `PlaybackRateRadioGroup.Value` displays the selected label, typically inside the trigger. `Options` and `Value` render `null` when the [playback rate feature](https://videojs.org/docs/framework/react/reference/api/feature-playback-rate) is not configured.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-rate` | `number` | Current playback rate. |
| `data-disabled` | Present / absent | Present when playback rate selection is disabled. |
| `data-hidden` | Present / absent | Present when playback-rate selection is unavailable. |
| `data-availability` | `"available"` / `"unavailable"` | Whether playback rates are available. |
Unavailable groups receive the native `hidden` attribute.
## Accessibility
The group uses the menu radio group pattern.
`PlaybackRateRadioGroup.Options` receives its accessible label from the `label` prop on `PlaybackRateRadioGroup.Root` or defaults to `Playback rate`. Override it with `aria-label` or `aria-labelledby` on `PlaybackRateRadioGroup.Options`.
## Examples
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, Menu, PlaybackRateRadioGroup } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
function SpeedMenu(): ReactNode {
return (
}>
Speed
(
{item.label}
✓
)}
/>
);
}
export default function BasicUsage() {
return (
);
}
```
**App.css**
```css
.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;
}
```
## API Reference
### Root
Owns playback-rate option state and shares it with an enclosing menu. Does not render a DOM element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether playback rate selection is disabled. |
| `formatRate` | `((rate: number) => string)` | `formatPlaybackRate` | Custom formatter for visible playback rate labels. |
| `label` | `{ key: string; text: string } \| string \| ((state: PlaybackRateRadioGroupState) => Text \| string)` | `''` | Custom label for the options group. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `label` | `{ key: string; text: string } \| string` | |
| `value` | `string` | Current radio-group value. |
| `options` | `readonly Option[]` | Ordered options displayed by platform adapters. |
| `disabled` | `boolean` | Whether the entire option group is disabled. |
| `hidden` | `boolean` | Whether the option group is hidden because no meaningful selection is available. |
| `availability` | `'available' \| 'unavailable'` | Whether the media exposes a meaningful selection. |
| `rate` | `number` | |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-rate` | `number` | Current playback rate. |
| `data-disabled` | — | Present when playback rate selection is disabled. |
| `data-hidden` | — | Present when playback rate selection is unavailable. |
| `data-availability` | `'available' \| 'unavailable'` | Indicates playback rate availability (`available` or `unavailable`). |
### Options
Renders menu radio items for the player's available playback rates.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: PlaybackRateRadioGroupState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: PlaybackRateRadioGroupState) => ReactElement \| null)` | — | Render prop for custom element. |
| `renderItem` | `((props: PlaybackRateRadioGroupItemProps, state: PlaybackRateRadioGroupItemState) => ReactElement)` | — | Render one consumer-owned menu radio item for every playback rate. |
| `style` | `CSSProperties \| ((state: PlaybackRateRadioGroupState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Value
Displays the selected playback-rate label.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: PlaybackRateOptionsResult) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: PlaybackRateOptionsResult) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: PlaybackRateOptionsResult) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# QualityRadioGroup
A menu radio group for selecting video quality
Creates radio items from the player video rendition state and selects automatic or manual quality.
## Import
```tsx
import { QualityRadioGroup } from '@videojs/react';
```
## Anatomy
```tsx
(
{item.label}
)}
/>
```
## Behavior
The group is available when the configured media exposes more than one video rendition. The first generated option is `Auto`; selecting it returns control to adaptive bitrate selection. Pass `formatRendition` to customize visible rendition labels.
`QualityRadioGroup.Root` uses [`useQualityOptions`](https://videojs.org/docs/framework/react/reference/api/use-quality-options) 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`. `QualityRadioGroup.Options` renders the items: its required `renderItem` callback renders the [`Menu.RadioItem`](https://videojs.org/docs/framework/react/reference/components/menu) root and receives item state containing the translated `label`, optional `tier` and `badge`, and current `checked` value. `QualityRadioGroup.Value` displays the selected label, typically inside the trigger. `Options` and `Value` render `null` when the [quality feature](https://videojs.org/docs/framework/react/reference/api/feature-quality) is not configured.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-quality` | `string` | Current quality value. |
| `data-disabled` | Present / absent | Present when quality selection is disabled. |
| `data-hidden` | Present / absent | Present when multiple renditions are unavailable. |
| `data-availability` | `"available"` / `"unavailable"` | Whether multiple renditions are available. |
Unavailable groups receive the native `hidden` attribute.
## Accessibility
The group uses the menu radio group pattern.
`QualityRadioGroup.Options` receives its accessible label from the `label` prop on `QualityRadioGroup.Root` or defaults to `Quality`. Override it with `aria-label` or `aria-labelledby` on `QualityRadioGroup.Options`.
## Examples
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, Menu, QualityRadioGroup } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8';
function QualityMenu(): ReactNode {
return (
}>
Quality
(
{item.label}
{item.tier ? {item.tier} : null}
{item.badge ? {item.badge} : null}
✓
)}
/>
);
}
export default function BasicUsage() {
return (
);
}
```
**App.css**
```css
.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-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
[role="menuitemradio"][aria-checked="true"] .menu-indicator {
opacity: 1;
}
```
## API Reference
### Root
Owns quality option state and shares it with an enclosing menu. Does not render a DOM element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether quality selection is disabled. |
| `formatRendition` | `((rendition: MediaVideoRendition) => Text \| string)` | `formatRenditionLabel` | Custom formatter for visible rendition labels. |
| `label` | `{ key: string; text: string } \| string \| ((state: QualityRadioGroupState) => Text \| string)` | `''` | Custom label for the options group. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `label` | `{ key: string; text: string } \| string` | |
| `value` | `string` | Current radio-group value. |
| `options` | `readonly Option[]` | Ordered options displayed by platform adapters. |
| `disabled` | `boolean` | Whether the entire option group is disabled. |
| `hidden` | `boolean` | Whether the option group is hidden because no meaningful selection is available. |
| `availability` | `'available' \| 'unavailable'` | Whether the media exposes a meaningful selection. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-quality` | `string` | Current quality value. |
| `data-disabled` | — | Present when quality selection is disabled. |
| `data-hidden` | — | Present when quality selection is unavailable. |
| `data-availability` | `'available' \| 'unavailable'` | Indicates quality availability (`available` or `unavailable`). |
### Options
Renders menu radio items for the player's video renditions.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: QualityRadioGroupState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: QualityRadioGroupState) => ReactElement \| null)` | — | Render prop for custom element. |
| `renderItem` | `((props: QualityRadioGroupItemProps, state: QualityRadioGroupItemState) => ReactElement)` | — | Render one consumer-owned menu radio item for every quality option. |
| `style` | `CSSProperties \| ((state: QualityRadioGroupState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Value
Displays the selected quality label.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: QualityOptionsResult) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: QualityOptionsResult) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: QualityOptionsResult) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# Time
Time display components for showing current time, duration, and remaining time in a video player
## Import
```tsx
import { Time } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
Three display types — `current`, `duration`, and `remaining` — in digital format with smart padding:
- **Hours** are never padded (`1:05:30`, not `01:05:30`)
- **Minutes** are padded when hours are shown (`1:05:30`, but `5:30`)
- **Seconds** are always padded (`1:05`, not `1:5`)
Hour display is triggered when either the current value or the duration exceeds 1 hour, ensuring consistency within a Group. Remaining time displays a negative sign (customizable via the `negativeSign` prop).
Use `toggle` to let current displays switch between elapsed and remaining time, or remaining and duration displays switch between those two values. The initial display comes from `type`.
Before the media reports a duration or seekable range — while metadata is still loading, for example — no time value is available. A plain display still renders the formatted zero value but sets `data-unavailable`; a toggleable display is disabled instead: it sets `data-disabled`, leaves the tab order, and ignores activation until a value arrives.
```tsx
```
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-type` | `current` / `duration` / `remaining` | The type of time being displayed |
| `data-disabled` | Present / absent | Present when a toggleable display has no time value yet |
| `data-unavailable` | Present / absent | Present when a plain display has no time value yet |
The negative sign is rendered inside `
` and can be hidden with CSS:
```css
[data-type="remaining"] > span[aria-hidden] {
display: none;
}
```
Dim a display while its value is unavailable:
```css
[data-disabled],
[data-unavailable] {
opacity: 0.5;
}
```
## Accessibility
Each `` has:
- a native `` element for machine-readable time semantics
- `aria-label` for the static role label (“Current time”, “Duration”, “Remaining”)
No `aria-live` region is used — time updates too frequently and might overwhelm screen readers. The separator and negative sign are `aria-hidden="true"` because the accessible label already describes the value.
Toggleable time displays receive `role="button"`, `tabindex="0"`, and keyboard support for Enter and Space. Their `aria-label` starts with the action and includes the current value. Their `aria-description` explains which values the control toggles between.
While no time value is available, a toggleable display is disabled: it sets `aria-disabled="true"` and `tabindex="-1"`, drops the `aria-description`, changes its `aria-label` to “Media not loaded, unknown time.”, and ignores clicks and key presses. A plain display in the same state omits `datetime` and uses that same label.
## Examples
### Current Time
**App.tsx**
```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function CurrentTime() {
return (
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-time {
position: absolute;
bottom: 10px;
left: 10px;
padding-block: 8px;
padding-inline: 20px;
font-variant-numeric: tabular-nums;
color: black;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
### Current / Duration
**App.tsx**
```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function CurrentDuration() {
return (
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.time-group {
position: absolute;
bottom: 10px;
left: 10px;
display: flex;
gap: 4px;
align-items: center;
padding-block: 8px;
padding-inline: 20px;
font-variant-numeric: tabular-nums;
color: black;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
### Remaining
**App.tsx**
```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function Remaining() {
return (
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-time {
position: absolute;
bottom: 10px;
left: 10px;
padding-block: 8px;
padding-inline: 20px;
font-variant-numeric: tabular-nums;
color: black;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
### Custom Separator
**App.tsx**
```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function CustomSeparator() {
return (
of
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.time-group {
position: absolute;
bottom: 10px;
left: 10px;
display: flex;
gap: 4px;
align-items: center;
padding-block: 8px;
padding-inline: 20px;
font-variant-numeric: tabular-nums;
color: black;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
### Custom Negative Sign
**App.tsx**
```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function CustomNegativeSign() {
return (
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-time {
position: absolute;
bottom: 10px;
left: 10px;
padding-block: 8px;
padding-inline: 20px;
font-variant-numeric: tabular-nums;
color: black;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
## API Reference
### Value
Displays a formatted time value (current, duration, or remaining).
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `{ key: string; text: string } \| string \| ((state: TimeState) => Text \| string)` | `''` | Custom label for accessibility. |
| `negativeSign` | `string` | `'-'` | Symbol prepended to remaining time. |
| `toggle` | `boolean` | `false` | Whether the time display can be toggled. |
| `type` | `'current' \| 'duration' \| 'remaining'` | `'current'` | Which time value to display. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `type` | `'current' \| 'duration' \| 'remaining'` | Time display type. |
| `disabled` | `boolean` | Whether the time toggle is disabled. |
| `unavailable` | `boolean` | Whether the non-interactive time value is unavailable. |
| `seconds` | `number` | Raw value in seconds. |
| `negative` | `boolean` | Whether the time value is negative (remaining time before end). |
| `text` | `string` | Formatted display text without sign (e.g., "1:30"). |
| `phrase` | `string` | Human-readable phrase (e.g., "1 minute, 30 seconds"). |
| `datetime` | `string` | ISO 8601 duration (e.g., "PT1M30S"). |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-type` | `'current' \| 'duration' \| 'remaining'` | The type of time being displayed. |
| `data-disabled` | — | Present when the time toggle is disabled. |
| `data-unavailable` | — | Present when the non-interactive time value is unavailable. |
### Group
Container for composed time displays. Renders a `` element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Time value components to render inside the group. |
| `className` | `string \| ((state: Record) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: Record) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: Record) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Separator
Divider between time values. Hidden from screen readers.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Separator content. Defaults to "/". |
| `className` | `string \| ((state: Record) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: Record) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: Record) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# Title
Displays the resolved content title for the current media
## Import
```tsx
import { Title } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
The component renders the resolved title directly as text and owns that text: it does not accept `children`. Set the title on the player instead.
```tsx
```
A title can come from two places. The player uses the first one that has a value:
1. The title you set on the player.
2. The title the media reports about itself.
If neither has a value, there is nothing to show and the title hides itself.
The title is player input, not player state, so change it the way you set it in the first place. The store exposes the resolved `title` to read, and nothing to write. That resolution belongs to the [metadata feature](https://videojs.org/docs/framework/react/reference/api/feature-metadata), which the video and audio presets include.
```tsx
const [title, setTitle] = useState("Big Buck Bunny");
... ;
```
Set it to `null` to hand the title back to the media.
The component exposes `data-visible` while controls are visible. Use that hook to animate the title with the controls.
## Styling
React renders a ``, or nothing at all when no title resolves. Add a `className` to style it:
```css
.title {
opacity: 0;
transition: opacity 150ms ease-out;
}
```
To fade the title in and out with the controls, use the root’s `data-visible` attribute:
```css
.title[data-visible] {
opacity: 1;
}
```
`className` and `style` also accept a function of component state when you would rather branch in JavaScript than in CSS:
```tsx
(state.title.length > 40 ? "title title--long" : "title")} />
```
## Accessibility
The title is ordinary text, so it reaches assistive technology as content with no ARIA of its own. The native `hidden` attribute it sets when there is no title also removes it from the accessibility tree, so nothing is announced when there is nothing to announce.
Fading the title out with `opacity` leaves it readable by a screen reader while it is not painted. That is deliberate — the name of what is playing stays relevant whether or not the controls happen to be on screen.
The component does not name the player. To give the player region an accessible name, set `aria-label` or `aria-labelledby` on the container yourself.
## Examples
### Basic Usage
**App.tsx**
```tsx
import { Container, createPlayer, PlayButton, Title } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
{state.paused ? 'Play' : 'Pause'} }
/>
);
}
```
**App.css**
```css
.react-title-basic {
position: relative;
display: block;
}
.react-title-basic video {
display: block;
width: 100%;
}
/* React renders nothing until a title resolves, so the gradient never sits
over empty space without this demo doing anything. */
.react-title-basic__title {
position: absolute;
inset-inline: 0;
top: 0;
padding: 16px 20px 48px;
font-size: 16px;
font-weight: 500;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
pointer-events: none;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);
}
.react-title-basic__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);
}
```
## API Reference
### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `title` | `string` | The resolved content title. Empty when no source supplied one. |
| `hidden` | `boolean` | Whether the component is hidden because no title is available. |
| `visible` | `boolean` | Whether the player controls are visible. |
### Data attributes
| Attribute | Description |
| --- | --- |
| `data-hidden` | Present when the element is hidden because no title is available. |
| `data-visible` | Present while the player controls are visible. |
---
# Poster
Poster image component that displays a thumbnail until video playback starts
## Import
```tsx
import { Poster } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
The poster shows before playback starts. Once the user plays or seeks, it hides; pausing does not bring it back. Loading a new source shows it again.
You can set the poster URL on the player. This is useful when you want to change the poster from inside a skin, or keep all your media metadata in one place:
```tsx
```
## Supply your own image
The component fills in a source only when you leave one out. `srcset`, `sizes`, `loading`, ``, and framework image components all stay available.
`Poster.Root` owns visibility and loading state. Image attributes go on `Poster.Image`:
```tsx
```
To render a different image component, use `render` on `Poster.Image`. It receives the poster URL as `src`, undefined until one resolves:
```tsx
) =>
src ? : null
}
/>
```
Inside a skin, pass the same function as `renderPoster`. The skin draws your image in place of its own:
```tsx
) =>
src ? : null
}
/>
```
A skin styles the ` ` it draws directly. Render something that is not an ` ` and its sizing is yours.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-visible` | Present / absent | Present before playback starts |
| `data-loading` | Present / absent | Present while the image is fetching |
| `data-loaded` | Present / absent | Present once the image has loaded |
| `data-error` | Present / absent | Present when the image failed |
The three load attributes track the image on screen, including one you supplied yourself.
React renders a root `` around the `
`. The state attributes are on `Poster.Root`, so other presentation layers can use the same lifecycle:
```tsx
```
```css
.media-poster:not([data-visible]) {
opacity: 0;
}
.media-poster:not([data-loading]) .media-poster-blur {
opacity: 0;
}
```
## Accessibility
Unlike the native `
` attribute, this component lets you describe the poster for screen readers.
The image is decorative by default (`alt=""`), since a URL says nothing about what the image shows. When the poster carries meaning, say so:
```tsx
```
Whether a poster is informative or decorative is your judgment, per the [WAI guidelines](https://www.w3.org/WAI/tutorials/images/decorative/).
## Examples
### Basic Usage
**App.tsx**
```tsx
import { Container, createPlayer, PlayButton, Poster } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
{state.paused ? 'Play' : 'Pause'} }
/>
);
}
```
**App.css**
```css
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-poster {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
transition: opacity 0.25s;
}
.media-poster:not([data-visible]) {
opacity: 0;
}
.media-poster-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.media-play-button {
position: absolute;
bottom: 10px;
left: 10px;
padding-block: 8px;
padding-inline: 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
```
## API Reference
### Root
Manages poster visibility and image loading state.
Renders a `div` and exposes `data-visible`, `data-loading`, `data-loaded`, and `data-error` for styling every layer in the poster presentation. Render `Poster.Image` inside it for the image that supplies the loading lifecycle.
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `visible` | `boolean` | Whether the poster should be shown, which it is until playback starts. |
| `src` | `string` | Resolved poster URL, empty when nothing supplied one. |
| `loading` | `boolean` | Whether the poster image is fetching. |
| `loaded` | `boolean` | Whether the poster image has decoded. |
| `error` | `boolean` | Whether the poster image failed. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-visible` | Present until playback starts. |
| `data-loading` | Present while the poster image is fetching. |
| `data-loaded` | Present once the poster image has decoded. |
| `data-error` | Present when the poster image failed. |
### Image
Displays the poster image managed by `Poster.Root`.
Renders an `img`, so `srcSet`, `sizes`, `loading`, and the rest of the native image attributes remain available. Leave the source off and the player's resolved poster fills it in. The image is decorative unless you supply `alt`.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: PosterState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: PosterState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: PosterState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# Thumbnail
Time-based thumbnail preview component for timeline scrubbing and hover previews
## Quick Start: Video Track
`Thumbnail` can read thumbnail cues directly from your video track. Add a `` with `kind="metadata"` and `label="thumbnails"` to your media component.
[Mux](https://www.mux.com/?utm_source=videojs&utm_campaign=vjs10&utm_content=docs-content) provides this as `storyboard.vtt`:
`https://image.mux.com/{PLAYBACK_ID}/storyboard.vtt`
That track is cross-origin, and a cross-origin `` only loads when the media component is CORS-enabled:
```tsx
```
A same-origin track needs none of this.
## Import
```tsx
import { Thumbnail } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
The thumbnail root resolves an image for the current `time`, owns `src` and `srcset` on it, and reports its loading lifecycle.
Render that image with `Thumbnail.Image`.
Supported source formats:
- Text track: ``
- JSON array: `{ url, startTime, endTime? }[]`
- JSON sprite array: `{ url, startTime, endTime?, width, height, coords }[]`
Text-track mode needs `Player` because it reads track state from the player store. JSON modes (`thumbnails` prop) work without a player.
The component picks the latest thumbnail whose `startTime` is less than or equal to the current `time`, then scales/clips sprite tiles to fill CSS min/max constraints while preserving aspect ratio. Tiles scale up as well as down, so a preview whose `max-width` grows — a container query widening it in fullscreen, say — grows with it.
### Cross-origin images
Leave `crossOrigin` unset and the thumbnail follows the media component. A cross-origin thumbnail `` only loads when the media is CORS-enabled, so the images its cues point at are fetched with that same mode. Skins get this for free, with nothing to thread through.
Opt out to fetch them without CORS:
```tsx
```
An empty value does not opt out either. The [CORS settings attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) reads anything other than `use-credentials` as Anonymous, so it is a value like any other.
Thumbnails you supply through `thumbnails` never inherit, since they need not be related to the media component at all. Set `crossOrigin` yourself when those images need it.
## Styling
Use state data attributes for pure CSS styling:
React renders a root `` around an `
`. State attributes belong to `Thumbnail.Root`, while image attributes and `render` belong to `Thumbnail.Image`:
```tsx
```
The root clips to the selected tile while the image inside spans the whole sprite sheet, so anything after the image in flow lands past the clip edge. Position the root and lay overlays over it:
```css
.media-thumbnail {
position: relative;
}
.media-thumbnail-overlay {
position: absolute;
inset: 0;
}
.media-thumbnail[data-hidden] {
display: none;
}
.media-thumbnail[data-loading] {
opacity: 0.6;
}
.media-thumbnail[data-error] {
outline: 1px solid #ef4444;
}
```
## Accessibility
`Thumbnail.Root` is decorative by default (`aria-hidden="true"`). It is intended for visual preview UX (for example, timeline hover previews) rather than primary accessible content.
## Examples
### Text Track (VTT)
**App.tsx**
```tsx
import { Container, createPlayer, Thumbnail } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function TextTrackUsage() {
return (
);
}
```
**App.css**
```css
.demo {
position: relative;
max-width: 280px;
}
.media {
position: absolute;
width: 1px;
height: 1px;
pointer-events: none;
opacity: 0;
}
.media-thumbnail {
display: block;
width: auto;
min-width: 0;
max-width: 240px;
}
.media-thumbnail-image {
display: block;
}
.media-thumbnail[data-hidden] {
display: none;
}
```
### JSON Array
**App.tsx**
```tsx
import { Thumbnail } from '@videojs/react';
const THUMBNAILS = [
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=0',
startTime: 0,
endTime: 10,
},
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=10',
startTime: 10,
endTime: 20,
},
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg?time=20',
startTime: 20,
},
];
export default function JsonUsage() {
return (
);
}
```
### JSON Sprite Array
**App.tsx**
```tsx
import { Thumbnail } from '@videojs/react';
const THUMBNAILS = [
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
startTime: 0,
endTime: 10,
width: 284,
height: 160,
coords: { x: 0, y: 0 },
},
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
startTime: 10,
endTime: 20,
width: 284,
height: 160,
coords: { x: 284, y: 0 },
},
{
url: 'https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.jpg',
startTime: 20,
width: 284,
height: 160,
coords: { x: 568, y: 0 },
},
];
export default function JsonSpriteUsage() {
return (
);
}
```
## API Reference
### Root
Resolves, sizes, and clips a thumbnail for a point in time.
Renders a `div` and exposes `data-hidden`, `data-loading`, and `data-error` for styling every layer in the preview. Render `Thumbnail.Image` inside it for the image the root controls and measures.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `thumbnails` | `ThumbnailImage[]` | — | Pre-parsed thumbnail images — bypasses the automatic `
` detection. |
| `time` | `number` | — | Time in seconds to display the thumbnail for. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `loading` | `boolean` | The thumbnail image is loading. |
| `error` | `boolean` | The thumbnail image failed to load. |
| `hidden` | `boolean` | Whether the component is hidden because no thumbnail is available and it is not loading. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-loading` | |
| `data-error` | |
| `data-hidden` | |
### Image
Displays the image selected and measured by `Thumbnail.Root`.
Renders an `img`, so native image attributes and the `render` escape hatch remain available without replacing the root that owns thumbnail state.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: ThumbnailState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `crossOrigin` | `ThumbnailCrossOrigin` | — | CORS setting for the selected image. Leave unset to follow the media component, or pass `null` to opt out. |
| `fetchPriority` | `ThumbnailFetchPriority` | — | Image fetch priority hint. |
| `loading` | `ThumbnailLoading` | — | Image loading strategy. |
| `render` | `ReactElement \| ((props: HTMLProps, state: ThumbnailState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: ThumbnailState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# Tooltip
A tooltip component for displaying contextual labels on hover and focus
## Import
```tsx
import { Tooltip } from '@videojs/react';
```
## Anatomy
```tsx
Hover me
Label text
K
```
## Behavior
Displays a short label anchored to a trigger element. Opens after a configurable `delay` (default 600ms) on hover or immediately on focus. Closes when the pointer leaves or focus moves away, with an optional `closeDelay`.
The `side` and `align` props control the preferred placement relative to the trigger. When the preferred side overflows the positioning boundary, the tooltip uses the opposite side if it has more space. Positioning uses CSS Anchor Positioning where supported, with a JavaScript measurement fallback.
The component is composed from six parts: `Root` manages state and context, `Trigger` renders a button that activates the tooltip, `Popup` contains the label content, `Label` renders the tooltip body, `Shortcut` renders an optional keyboard hint, and `Arrow` renders a decorative pointer. Wrap multiple tooltips in a `Tooltip.Provider` to coordinate open/close timing across a group — once a tooltip becomes visible, adjacent tooltips open instantly within the `timeout` window, skipping the normal `delay`.
Tooltips inside a [player container](https://videojs.org/docs/framework/react/reference/components/player-container) also observe its popup group. By default, a tooltip does not open—or closes if already open—while a menu or popover attached to the same trigger is open. Set `sticky` when the tooltip should remain visible with that trigger’s popup.
`Tooltip.Provider` coordinates tooltip delay timing only; it does not replace the container’s popup group.
## Styling
Use [CSS custom properties](https://videojs.org/docs/framework/react/reference/components/tooltip#root-css-custom-properties) for positioning offsets:
React renders standard DOM elements. Add a `className` to style them:
```css
.tooltip-popup {
--media-tooltip-side-offset: 8px;
--media-tooltip-align-offset: 0px;
--media-tooltip-boundary-offset: 8px;
}
```
Style based on open state, rendered side, and transition phases. `data-side` reflects the rendered side and can differ from the preferred `side` prop after collision handling:
```css
.tooltip-popup[data-open] {
display: block;
}
.tooltip-popup[data-starting-style] {
opacity: 0;
}
.tooltip-popup[data-ending-style] {
opacity: 0;
}
.tooltip-popup[data-side="top"] {
transform-origin: bottom center;
}
.tooltip-popup[data-side="bottom"] {
transform-origin: top center;
}
```
## Accessibility
Tooltips are visual-only, supplementary labels. The popup renders with `role="presentation"` and is not referenced by the trigger with `aria-describedby`. Give the trigger its own accessible name instead of relying on tooltip content. Built-in media buttons already expose a state-aware `aria-label`, and their tooltips reuse that translated label. Tooltips still open on focus so keyboard users who can see the label receive the same visual hint.
## Examples
### Basic Usage
**App.tsx**
```tsx
import { Tooltip } from '@videojs/react';
export default function BasicUsage() {
return (
Hover me
Tooltip content
);
}
```
**App.css**
```css
.demo {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.trigger {
padding: 6px 16px;
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-tooltip {
--media-tooltip-side-offset: 8px;
padding: 4px 10px;
margin: 0;
font-size: 13px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgba(0, 0, 0, 0.85);
border: 0;
border-radius: 6px;
backdrop-filter: blur(10px);
}
.arrow {
fill: rgba(0, 0, 0, 0.85);
}
```
### Grouping
Wrap multiple tooltips in a `Tooltip.Provider` to share a delay group. Once a tooltip becomes visible, adjacent tooltips open instantly within the `timeout` window, skipping the normal `delay`.
**App.tsx**
```tsx
import { Tooltip } from '@videojs/react';
export default function Grouping() {
return (
Play
Play video
Mute
Mute audio
Fullscreen
Enter fullscreen
);
}
```
**App.css**
```css
.demo {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
}
.media-tooltip {
--media-tooltip-side-offset: 8px;
padding: 4px 10px;
margin: 0;
font-size: 13px;
color: white;
white-space: nowrap;
pointer-events: none;
background: rgba(0, 0, 0, 0.85);
border: 0;
border-radius: 6px;
backdrop-filter: blur(10px);
}
.arrow {
fill: rgba(0, 0, 0, 0.85);
}
```
## API Reference
### Provider
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeDelay` | `number` | — | Default close delay in ms for tooltips in this group. |
| `delay` | `number` | — | Default open delay in ms for tooltips in this group. |
| `timeout` | `number` | — | Duration in ms after a tooltip closes during which the next tooltip opens instantly. |
### Root
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `'start' \| 'center' \| 'end'` | `'center'` | Alignment of the tooltip along the trigger's edge. |
| `boundary` | `'viewport' \| 'container' \| string & {}` | — | Boundary used to constrain the tooltip position. |
| `closeDelay` | `number` | `0` | Delay in ms before closing after pointer leaves. |
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled usage. |
| `delay` | `number` | `600` | Delay in ms before opening on hover. |
| `disabled` | `boolean` | `false` | When true, the tooltip is disabled and will not open. |
| `disableHoverablePopup` | `boolean` | `true` | When true, hovering the popup does not keep it open. |
| `open` | `boolean` | `false` | Controlled open state. |
| `side` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Preferred side of the trigger for the tooltip. |
| `sticky` | `boolean` | `false` | Whether the tooltip stays open when another popup opens from its trigger. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the tooltip is currently visible. |
| `status` | `'idle' \| 'starting' \| 'ending'` | Current phase of the transition lifecycle. |
| `side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Preferred side of the trigger for the tooltip. |
| `align` | `'start' \| 'center' \| 'end'` | How the tooltip is aligned relative to the specified side. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the tooltip is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side of the tooltip after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the tooltip is aligned relative to the specified side. |
#### CSS custom properties
| Variable | Description |
| --- | --- |
| `--media-tooltip-side-offset` | Distance between the popup and the trigger along the side axis. |
| `--media-tooltip-align-offset` | Distance between the popup and the trigger along the alignment axis. |
| `--media-tooltip-boundary-offset` | Minimum distance between the popup and the positioning boundary. |
| `--media-tooltip-anchor-width` | The anchor element's width. |
| `--media-tooltip-anchor-height` | The anchor element's height. |
| `--media-tooltip-available-width` | Available width between the trigger and the boundary edge. |
| `--media-tooltip-available-height` | Available height between the trigger and the boundary edge. |
### Trigger
Element that triggers the tooltip on hover and focus. Renders a `` element.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: TooltipState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: TooltipState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: TooltipState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the tooltip is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side of the tooltip after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the tooltip is aligned relative to the specified side. |
### Popup
Container for the tooltip content. Positioned relative to the trigger using CSS anchor positioning with a JavaScript fallback.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: TooltipState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: TooltipState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: TooltipState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the tooltip is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side of the tooltip after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the tooltip is aligned relative to the specified side. |
### Label
Tooltip body label; defaults to context `content.label` from the linked trigger.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: TooltipState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: TooltipState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: TooltipState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the tooltip is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side of the tooltip after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the tooltip is aligned relative to the specified side. |
### Shortcut
Keyboard shortcut hint; apply skin `className` (CSS: `media-tooltip__kbd`; Tailwind: `popup.tooltipShortcut`).
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: TooltipState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: TooltipState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: TooltipState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present when the tooltip is open. |
| `data-side` | `'top' \| 'bottom' \| 'left' \| 'right'` | Indicates the rendered side of the tooltip after collision handling. |
| `data-align` | `'start' \| 'center' \| 'end'` | Indicates how the tooltip is aligned relative to the specified side. |
### Arrow
Decorative arrow pointing from the tooltip toward the trigger. Hidden from assistive technology.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: TooltipState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: TooltipState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: TooltipState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# BufferingIndicator
Loading indicator that displays when the video player is buffering or waiting for data
## Import
```tsx
import { BufferingIndicator } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
Shows a loading indicator when the media is waiting to buffer and not paused, but only after a configurable `delay` (default 500ms). This delay prevents the indicator from flickering during brief stalls. The indicator hides immediately when buffering ends.
## Styling
Hide and show the indicator based on the `data-visible` attribute.
## Examples
### Basic Usage
**App.tsx**
```tsx
import { BufferingIndicator, Container, createPlayer } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
}
/>
);
}
```
**App.css**
```css
.media-container {
position: relative;
display: flex;
width: 100%;
height: 100%;
}
.media-buffering-indicator {
position: absolute;
inset: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.spinner {
display: none;
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: buffering-spin 0.8s linear infinite;
}
.media-buffering-indicator[data-visible] .spinner {
display: block;
}
@keyframes buffering-spin {
to {
transform: rotate(360deg);
}
}
```
## API Reference
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delay` | `number` | `500` | Delay in milliseconds before the indicator becomes visible. |
### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `visible` | `boolean` | Whether the indicator should be visible. True after the delay elapses while media is waiting and not paused. |
### Data attributes
| Attribute | Description |
| --- | --- |
| `data-visible` | Present when the buffering indicator is visible (after delay). |
---
# SeekIndicator
A temporary visual indicator for keyboard and gesture seeking
## Import
```tsx
import { SeekIndicator } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`SeekIndicator` displays feedback for seek actions emitted by a [`Hotkey`](https://videojs.org/docs/framework/react/reference/components/hotkey) or [`Gesture`](https://videojs.org/docs/framework/react/reference/components/gesture) in the same player `Container`. It does not react to current-time changes or seek commands invoked directly through the player store.
| Action | Direction | Value |
| --- | --- | --- |
| `seekStep` | The sign of `value` determines `forward` or `backward` | The absolute number of seconds, such as `10s` |
| `seekToPercent` | The target percentage is compared with the current time | The formatted current time when the action occurred |
Rapid `seekStep` actions in the same direction accumulate while the indicator is open. For example, two 10-second forward actions display `20s`. Changing direction or using `seekToPercent` starts a new display. During a rapid sequence, another full step is not added when it would pass the start or duration boundary.
For `seekToPercent`, pass a percentage from 0 to 100 as `value`. A `Hotkey` with `keys="0-9"` can omit `value`; the pressed digit maps to 0%, 10%, and so on through 90%.
The indicator closes after `closeDelay`, which defaults to 800 milliseconds.
`SeekIndicator.Root` stops rendering after its close transition.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-open` | Present / absent | Present while the indicator is open |
| `data-direction` | `"forward"` \| `"backward"` | Direction of the handled seek |
| `data-starting-style` | Present / absent | Present during the open transition |
| `data-ending-style` | Present / absent | Present during the close transition |
Position the root from `data-direction` and use the transition attributes for entry and exit styles.
React renders standard DOM elements. Add a `className` to the Root:
```css
.seek-indicator[data-direction="backward"] {
left: 1rem;
}
.seek-indicator[data-direction="forward"] {
right: 1rem;
}
.seek-indicator[data-starting-style],
.seek-indicator[data-ending-style] {
opacity: 0;
}
```
## Accessibility
`SeekIndicator` is visual feedback and does not create a live region. Keep the same seek operation available through keyboard-operable controls, and pair it with [`StatusAnnouncer`](https://videojs.org/docs/framework/react/reference/components/status-announcer) when changes should be announced to screen readers. Do not make `SeekIndicator.Value` a live region because rapid seek input would produce repeated announcements.
## Examples
### Basic Usage
Focus the player, then press the left or right arrow key to seek by ten seconds. Press a digit from 0 through 9 to seek to a percentage of the duration.
**App.tsx**
```tsx
import { Container, createPlayer, Hotkey, SeekIndicator } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
Focus the player · ←/→: seek 10s · 0–9: seek to percent
);
}
```
**App.css**
```css
.react-seek-indicator-basic {
position: relative;
}
.react-seek-indicator-basic:focus-visible {
outline: 3px solid #60a5fa;
outline-offset: 2px;
}
.react-seek-indicator-basic video {
display: block;
width: 100%;
}
.react-seek-indicator-basic__instructions {
position: absolute;
top: 10px;
left: 50%;
padding: 6px 10px;
margin: 0;
color: white;
text-align: center;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
translate: -50%;
}
.react-seek-indicator-basic__indicator {
position: absolute;
top: 50%;
left: 50%;
display: grid;
min-width: 72px;
padding: 16px;
color: white;
pointer-events: none;
background: rgb(0 0 0 / 72%);
border-radius: 9999px;
place-items: center;
transform: translate(-50%, -50%);
transition:
opacity 160ms ease-in-out,
scale 160ms ease-in-out;
}
.react-seek-indicator-basic__indicator::before {
font-size: 24px;
line-height: 1;
content: "↔";
}
.react-seek-indicator-basic__indicator[data-direction="backward"] {
right: auto;
left: 16px;
transform: translateY(-50%);
}
.react-seek-indicator-basic__indicator[data-direction="backward"]::before {
content: "↶";
}
.react-seek-indicator-basic__indicator[data-direction="forward"] {
right: 16px;
left: auto;
transform: translateY(-50%);
}
.react-seek-indicator-basic__indicator[data-direction="forward"]::before {
content: "↷";
}
.react-seek-indicator-basic__indicator[data-starting-style],
.react-seek-indicator-basic__indicator[data-ending-style] {
opacity: 0;
scale: 0.85;
}
.react-seek-indicator-basic__value {
font-variant-numeric: tabular-nums;
}
```
## API Reference
### Root
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeDelay` | `number` | — | Delay in milliseconds before the indicator closes. |
| `locale` | `string \| string[]` | — | |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the indicator is open. |
| `generation` | `number` | Increments each time a seek input action updates the indicator. |
| `direction` | `'forward' \| 'backward' \| null` | Direction of the seek, or `null` when the target does not change the current time. |
| `count` | `number` | Number of same-direction seek steps accumulated in the current display. |
| `seekTotal` | `number` | Absolute number of seconds accumulated from seek-step actions. |
| `value` | `string \| null` | Accumulated seek-step label, or `null` for percentage seeks. |
| `currentTime` | `string` | Formatted current time captured when the input action occurred. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present while the indicator is open. |
| `data-direction` | `'forward' \| 'backward' \| null` | Direction of the seek as `"forward"` or `"backward"`. |
| `data-starting-style` | — | Present during the open transition. |
| `data-ending-style` | — | Present during the close transition. |
### Value
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: SeekIndicatorState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: SeekIndicatorState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: SeekIndicatorState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# StatusIndicator
Display temporary visual feedback for keyboard and gesture actions
## Import
```tsx
import { StatusIndicator } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`StatusIndicator` displays feedback for actions emitted by a [`Hotkey`](https://videojs.org/docs/framework/react/reference/components/hotkey) or [`Gesture`](https://videojs.org/docs/framework/react/reference/components/gesture) in the same player `Container`. It does not react to buttons, direct player-store changes, or media state changes on their own.
The input event arrives before its action is resolved. The indicator uses the media snapshot from that moment to predict the next visual status:
| Action | `data-status` | Value |
| --- | --- | --- |
| `togglePaused` | `"play"` or `"pause"` | Translated playing or paused label |
| `toggleMuted` | `"volume-off"`, `"volume-low"`, or `"volume-high"` | Predicted volume percentage |
| `volumeStep` | `"volume-off"`, `"volume-low"`, or `"volume-high"` | Predicted volume percentage |
| `toggleSubtitles` | `"captions-on"` or `"captions-off"` | Translated captions label |
| `toggleFullscreen` | `"fullscreen"` or `"exit-fullscreen"` | Translated fullscreen label |
| `togglePictureInPicture` | `"pip"` or `"exit-pip"` | Translated picture-in-picture label |
`toggleSubtitles` is ignored when the player reports that no captions or subtitles are available. Seek actions, `toggleControls`, playback-rate actions, and custom actions do not open this indicator unless you [derive a custom status](https://videojs.org/docs/framework/react/reference/components/status-indicator#custom-actions).
Use `actions` to allow only some of the supported actions. Omitting it allows all supported actions.
Pass a readonly array of action names:
```tsx
```
The indicator closes after `closeDelay`, which defaults to 800 milliseconds. A repeated handled action updates the current value and restarts that close timer without replaying the entry transition.
`StatusIndicator.Root` stops rendering after its close transition.
## Custom actions
Use `deriveCustomStatus` to give custom hotkey or gesture actions the same feedback as built-in actions. The indicator calls it only when an allowed action has no built-in status. It receives the input event and the pre-action media snapshot. Return `{ status, label, value }` to open the indicator, or `null` to leave it closed. The returned `status` becomes `data-status`, and the Value part shows `value` when it is not `null` and `label` otherwise. Because the snapshot is taken before the action runs, predict the post-action state the way built-in statuses do.
Register the custom action with [`useHotkey`](https://videojs.org/docs/framework/react/reference/api/use-hotkey) and pass `action` so the indicator receives it. `Hotkey` only accepts built-in actions.
```tsx
import { selectPlaybackRate, StatusIndicator, useHotkey, usePlayer } from "@videojs/react";
const rates = [0.5, 1, 1.5, 2];
function getNextRate(rate = 1) {
const index = rates.indexOf(rate);
return rates[Math.min(index + 1, rates.length - 1)] ?? rate;
}
function RateHotkey() {
const rate = usePlayer(selectPlaybackRate);
useHotkey({
keys: ">",
action: "stepRate",
onActivate: () => rate?.setPlaybackRate(getNextRate(rate.playbackRate)),
});
// The hook must run inside a component, but this one has no UI to render.
return null;
}
function RateIndicator() {
return (
event.action === "stepRate"
? { status: "rate-up", label: `${getNextRate(snapshot.playbackRate)}×`, value: null }
: null
}
>
);
}
```
Render both components inside the same player `Container`.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-open` | Present / absent | Present while the indicator is open |
| `data-status` | `"play"`, `"pause"`, `"volume-off"`, `"volume-low"`, `"volume-high"`, `"captions-on"`, `"captions-off"`, `"fullscreen"`, `"exit-fullscreen"`, `"pip"`, `"exit-pip"`, or a custom status | Predicted status for the handled action |
| `data-starting-style` | Present / absent | Present during the open transition |
| `data-ending-style` | Present / absent | Present during the close transition |
Use `data-status` to select an icon or other visual treatment, and use the transition attributes for entry and exit styles.
React renders standard DOM elements. Add a `className` to the Root:
```css
.status-indicator[data-status="play"] {
color: green;
}
.status-indicator[data-starting-style],
.status-indicator[data-ending-style] {
opacity: 0;
}
```
## Accessibility
`StatusIndicator` is visual feedback and does not create a live region. Keep every action available through keyboard-operable controls, and pair the player with [`StatusAnnouncer`](https://videojs.org/docs/framework/react/reference/components/status-announcer) when state changes should be announced to screen readers. Do not make `StatusIndicator.Value` a live region.
## Examples
### Basic Usage
Focus the player, then press K to play or pause, M to mute, F for fullscreen, C for captions, or I for picture-in-picture.
**App.tsx**
```tsx
import { Container, createPlayer, Hotkey, StatusIndicator } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
const statusActions = [
'togglePaused',
'toggleMuted',
'toggleSubtitles',
'toggleFullscreen',
'togglePictureInPicture',
] as const;
export default function BasicUsage() {
return (
Focus the player · K: play/pause · M: mute · F: fullscreen · C: captions · I: picture-in-picture
);
}
```
**App.css**
```css
.react-status-indicator-basic {
position: relative;
}
.react-status-indicator-basic:focus-visible {
outline: 3px solid #60a5fa;
outline-offset: 2px;
}
.react-status-indicator-basic video {
display: block;
width: 100%;
}
.react-status-indicator-basic__instructions {
position: absolute;
top: 10px;
right: 10px;
left: 10px;
padding: 6px 10px;
margin: 0;
color: white;
text-align: center;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
}
.react-status-indicator-basic__indicator {
position: absolute;
top: 50%;
left: 50%;
display: grid;
min-width: 92px;
padding: 16px;
color: white;
pointer-events: none;
background: rgb(0 0 0 / 72%);
border-radius: 12px;
place-items: center;
transform: translate(-50%, -50%);
transition:
opacity 160ms ease-in-out,
scale 160ms ease-in-out;
}
.react-status-indicator-basic__indicator::before {
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.react-status-indicator-basic__indicator[data-status="play"]::before {
content: "▶";
}
.react-status-indicator-basic__indicator[data-status="pause"]::before {
content: "Ⅱ";
}
.react-status-indicator-basic__indicator[data-status="volume-off"]::before {
content: "🔇";
}
.react-status-indicator-basic__indicator[data-status="volume-low"]::before {
content: "🔉";
}
.react-status-indicator-basic__indicator[data-status="volume-high"]::before {
content: "🔊";
}
.react-status-indicator-basic__indicator[data-status="captions-on"]::before,
.react-status-indicator-basic__indicator[data-status="captions-off"]::before {
content: "CC";
}
.react-status-indicator-basic__indicator[data-status="captions-off"]::before {
text-decoration: line-through;
}
.react-status-indicator-basic__indicator[data-status="fullscreen"]::before {
content: "⛶";
}
.react-status-indicator-basic__indicator[data-status="exit-fullscreen"]::before {
content: "⊠";
}
.react-status-indicator-basic__indicator[data-status="pip"]::before,
.react-status-indicator-basic__indicator[data-status="exit-pip"]::before {
content: "▣";
}
.react-status-indicator-basic__indicator[data-status="exit-pip"]::before {
text-decoration: line-through;
}
.react-status-indicator-basic__indicator[data-starting-style],
.react-status-indicator-basic__indicator[data-ending-style] {
opacity: 0;
scale: 0.85;
}
.react-status-indicator-basic__value {
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
```
## API Reference
### Root
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `actions` | `readonly InputAction[]` | — | Input actions allowed to open the indicator. All supported actions are allowed when omitted. |
| `closeDelay` | `number` | — | Delay in milliseconds before the indicator closes. |
| `deriveCustomStatus` | `((event: InputActionEvent, snapshot: MediaSnapshot) => StatusDetails \| null)` | — | Derives display details for custom actions. Called only when the built-in derivation returns `null`; return `null` to leave the indicator closed. |
| `labels` | `Partial` | — | Internal translated label overrides supplied by framework adapters. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the indicator is open. |
| `generation` | `number` | Increments each time a supported input action updates the indicator. |
| `status` | `'pause' \| 'play' \| 'volume-off' \| 'volume-low' \| 'volume-high' \| 'captions-on' \| 'captions-off' \| 'fullscreen' \| 'exit-fullscreen' \| 'pip' \| 'exit-pip' \| string & {} \| null` | Visual status for the handled input action. |
| `label` | `string \| null` | Translated label for the predicted status. |
| `value` | `string \| null` | Predicted volume percentage for volume actions, otherwise `null`. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present while the indicator is open. |
| `data-status` | `'pause' \| 'play' \| 'volume-off' \| 'volume-low' \| 'volume-high' \| 'captions-on' \| 'captions-off' \| 'fullscreen' \| 'exit-fullscreen' \| 'pip' \| 'exit-pip' \| string & {} \| null` | Predicted visual status for the handled input action. |
| `data-starting-style` | — | Present during the open transition. |
| `data-ending-style` | — | Present during the close transition. |
### Value
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: StatusIndicatorState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: StatusIndicatorState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: StatusIndicatorState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# VolumeIndicator
Display temporary visual feedback for keyboard and gesture volume actions
## Import
```tsx
import { VolumeIndicator } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`VolumeIndicator` displays feedback for `toggleMuted` and `volumeStep` actions emitted by a [`Hotkey`](https://videojs.org/docs/framework/react/reference/components/hotkey) or [`Gesture`](https://videojs.org/docs/framework/react/reference/components/gesture) in the same player `Container`. It does not react to mute buttons, volume sliders, direct player-store changes, or volume changes on their own.
The input event arrives before its action is resolved. The indicator uses the media snapshot from that moment to predict the next mute state and volume. `toggleMuted` predicts the opposite mute state. `volumeStep` adds its `value` to the snapshot volume and clamps the result from 0 to 1. A step whose clamped result is above 0 also predicts that mute will clear.
The root exposes the predicted level:
| `data-level` | Predicted state |
| --- | --- |
| `"off"` | Muted or volume is 0 |
| `"low"` | Unmuted volume is greater than 0 and at most 0.5 |
| `"high"` | Unmuted volume is greater than 0.5 |
`Value` displays the rounded percentage from 0% through 100%. `Fill` receives the same percentage through `--media-volume-fill`. Nest `Value` inside `Fill` so the progress treatment and text form one visual unit.
The indicator closes after `closeDelay`, which defaults to 800 milliseconds. Repeated handled actions update the current value and restart that close timer without replaying the entry transition. Each update uses the latest media snapshot; the component does not independently accumulate volume steps.
`VolumeIndicator.Root` stops rendering after its close transition.
When a nonzero `volumeStep` cannot move past an already-clamped edge, `data-min` or `data-max` is present for 300 milliseconds. Hitting the same edge again briefly clears the attribute, then restores it on the next task so a CSS boundary animation can restart. Merely reaching 0% or 100% does not trigger the boundary attribute until another step tries to move farther.
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-open` | Present / absent | Present while the indicator is open |
| `data-level` | `"off"`, `"low"`, or `"high"` | Predicted volume level |
| `data-min` | Present / absent | Present briefly after a blocked downward step at minimum volume |
| `data-max` | Present / absent | Present briefly after a blocked upward step at maximum volume |
| `data-starting-style` | Present / absent | Present during the open transition |
| `data-ending-style` | Present / absent | Present during the close transition |
`--media-volume-fill` is set on the Fill part, not the Root. Read it from a Fill selector when sizing a progress treatment.
React renders standard DOM elements. Add separate `className` values to the Root and Fill:
```css
.volume-indicator__fill::before {
width: var(--media-volume-fill, 0%);
}
.volume-indicator[data-starting-style],
.volume-indicator[data-ending-style] {
opacity: 0;
}
```
## Accessibility
`VolumeIndicator` is visual feedback and does not create a live region. Keep volume and mute available through keyboard-operable controls, and pair the player with [`StatusAnnouncer`](https://videojs.org/docs/framework/react/reference/components/status-announcer) when state changes should be announced to screen readers. Do not make `VolumeIndicator.Value` a live region.
## Examples
### Basic Usage
Focus the player, then press M to mute or unmute. Use Arrow Up and Arrow Down to adjust volume by 5%.
**App.tsx**
```tsx
import { Container, createPlayer, Hotkey, VolumeIndicator } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
{
if (video) video.volume = 0.5;
}}
src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
autoPlay
muted
playsInline
loop
/>
Focus the player · M: mute · ↑/↓: volume ±5%
);
}
```
**App.css**
```css
.react-volume-indicator-basic {
position: relative;
}
.react-volume-indicator-basic:focus-visible {
outline: 3px solid #60a5fa;
outline-offset: 2px;
}
.react-volume-indicator-basic video {
display: block;
width: 100%;
}
.react-volume-indicator-basic__instructions {
position: absolute;
top: 10px;
left: 50%;
padding: 6px 10px;
margin: 0;
color: white;
text-align: center;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
translate: -50%;
}
.react-volume-indicator-basic__indicator {
position: absolute;
top: 50%;
left: 50%;
display: grid;
min-width: 220px;
padding: 18px 20px 34px;
color: white;
pointer-events: none;
background: rgb(0 0 0 / 72%);
border-radius: 12px;
place-items: center;
translate: -50% -50%;
transition:
opacity 160ms ease-in-out,
scale 160ms ease-in-out;
}
.react-volume-indicator-basic__indicator::before {
margin-bottom: 12px;
font-size: 28px;
line-height: 1;
}
.react-volume-indicator-basic__indicator[data-level="off"]::before {
content: "🔇";
}
.react-volume-indicator-basic__indicator[data-level="low"]::before {
content: "🔉";
}
.react-volume-indicator-basic__indicator[data-level="high"]::before {
content: "🔊";
}
.react-volume-indicator-basic__fill {
position: relative;
width: 180px;
height: 10px;
background: rgb(255 255 255 / 25%);
border-radius: 9999px;
}
.react-volume-indicator-basic__fill::before {
position: absolute;
inset-block: 0;
left: 0;
width: var(--media-volume-fill, 0%);
content: "";
background: currentColor;
border-radius: inherit;
transition: width 160ms linear;
}
.react-volume-indicator-basic__value {
position: absolute;
top: calc(100% + 6px);
left: 50%;
font-variant-numeric: tabular-nums;
translate: -50%;
}
.react-volume-indicator-basic__indicator[data-starting-style],
.react-volume-indicator-basic__indicator[data-ending-style] {
opacity: 0;
scale: 0.85;
}
@media (prefers-reduced-motion: no-preference) {
.react-volume-indicator-basic__indicator[data-min],
.react-volume-indicator-basic__indicator[data-max] {
animation: react-volume-indicator-basic-shake 300ms linear;
}
}
@keyframes react-volume-indicator-basic-shake {
20% {
translate: calc(-50% - 8px) -50%;
}
40% {
translate: calc(-50% + 6px) -50%;
}
60% {
translate: calc(-50% - 4px) -50%;
}
80% {
translate: calc(-50% + 2px) -50%;
}
}
```
## API Reference
### Root
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeDelay` | `number` | — | Delay in milliseconds before the indicator closes. |
| `labels` | `Partial` | — | Internal translated label overrides supplied by framework adapters. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the indicator is open. |
| `generation` | `number` | Increments each time a volume input action updates the indicator. |
| `level` | `'off' \| 'low' \| 'high' \| null` | Predicted volume level after the input action. |
| `value` | `string \| null` | Predicted volume formatted as a percentage. |
| `fill` | `string \| null` | Predicted volume percentage used by the Fill part. |
| `min` | `boolean` | Whether a downward step tried to move past minimum volume. |
| `max` | `boolean` | Whether an upward step tried to move past maximum volume. |
#### Data attributes
| Attribute | Type | Description |
| --- | --- | --- |
| `data-open` | — | Present while the indicator is open. |
| `data-level` | `'off' \| 'low' \| 'high' \| null` | Predicted volume level as `"off"`, `"low"`, or `"high"`. |
| `data-min` | — | Present briefly when a downward step cannot lower the volume further. |
| `data-max` | — | Present briefly when an upward step cannot raise the volume further. |
| `data-starting-style` | — | Present during the open transition. |
| `data-ending-style` | — | Present during the close transition. |
#### CSS custom properties
| Variable | Description |
| --- | --- |
| `--media-volume-fill` | Current predicted volume percentage, set on the Fill part. |
### Fill
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: VolumeIndicatorState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: VolumeIndicatorState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: VolumeIndicatorState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Value
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: VolumeIndicatorState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: VolumeIndicatorState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: VolumeIndicatorState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# Dialog
A modal dialog for application content, including players opened from a thumbnail
## Import
```tsx
import { Dialog } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`Dialog` provides the modal interaction and accessibility behavior while your application supplies the content and styling. It is the general-purpose replacement for a `ModalDialog` use case such as opening a player from a thumbnail.
Use `Dialog.Trigger` to open the dialog. Use `open` with `onOpenChange` for controlled state, or `defaultOpen` for uncontrolled state. `Root` provides state without rendering an element, while `Popup` renders only while the dialog is open or closing.
The close part and Escape close the dialog. Other buttons inside the dialog, including player controls, do not close it.
Your application decides what opening and closing mean for the media. The example starts playback after opening and pauses it as soon as the dialog closes. Handle the promise returned by `play()`, since browsers can reject autoplay when it is not allowed.
Use [`AlertDialog`](https://videojs.org/docs/framework/react/reference/components/alert-dialog) for urgent messages that require acknowledgement. [`ErrorDialog`](https://videojs.org/docs/framework/react/reference/components/error-dialog) is the player-error specialization. Both use the same dialog behavior.
## Styling
Use the open and transition data attributes to style the modal and its animations:
| Attribute | Description |
| --- | --- |
| `data-open` | Present while the dialog is open, including its closing transition |
| `data-starting-style` | Present at the start of the opening transition |
| `data-ending-style` | Present during the closing transition |
React renders the backdrop and popup as separate DOM elements. Add a `className` to each part and style their transitions independently:
```css
.dialog-backdrop[data-starting-style],
.dialog-backdrop[data-ending-style],
.dialog-popup[data-starting-style],
.dialog-popup[data-ending-style] {
opacity: 0;
}
```
## Accessibility
The popup uses `role="dialog"` and `aria-modal="true"`. Include `Dialog.Title` so the dialog always has an accessible name. `Dialog.Description` is optional. These parts receive generated IDs connected through `aria-labelledby` and `aria-describedby`. The trigger receives `aria-haspopup`, `aria-controls`, and `aria-expanded`.
When the dialog opens, focus moves to an element with `autofocus`, the first focusable element, or the popup itself. Tab and Shift + Tab keep focus within the dialog, and content outside the dialog becomes inert. Closing restores focus to the trigger, or to the element that was focused before the dialog opened.
Dialogs inside the same player container share modal coordination. Opening one closes the current dialog and dismisses any open menu, tooltip, or popover before focus moves into the new popup.
## Examples
### Player modal
Open the video from its thumbnail. Playback starts when the dialog opens and pauses when it closes.
**App.tsx**
```tsx
import { Container, createPlayer, Dialog } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import { useEffect, useRef, useState } from 'react';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
const [open, setOpen] = useState(false);
const videoRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (open) void video.play().catch(() => {});
else video.pause();
}, [open]);
return (
Play the video
Video title
A video opened from a thumbnail.
Close
);
}
```
**App.css**
```css
.react-dialog-basic {
position: relative;
min-height: 320px;
}
.react-dialog-basic__trigger {
display: grid;
gap: 10px;
width: min(100%, 420px);
padding: 0 0 12px;
overflow: hidden;
color: white;
cursor: pointer;
background: #111827;
border: 0;
border-radius: 12px;
}
.react-dialog-basic__trigger img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.react-dialog-basic__backdrop {
position: absolute;
inset: 0;
z-index: 1;
background: rgb(3 7 18 / 80%);
transition: opacity 200ms ease;
}
.react-dialog-basic__dialog {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
transition: opacity 150ms ease;
}
.react-dialog-basic__backdrop[data-starting-style],
.react-dialog-basic__backdrop[data-ending-style],
.react-dialog-basic__dialog[data-starting-style],
.react-dialog-basic__dialog[data-ending-style] {
opacity: 0;
}
.react-dialog-basic__popup {
position: relative;
width: 100%;
max-width: 560px;
padding: 16px;
color: white;
background: #111827;
border-radius: 12px;
}
.react-dialog-basic__title {
margin: 0 0 4px;
font-size: 1.125rem;
font-weight: 600;
}
.react-dialog-basic__description {
margin: 0 0 12px;
}
.react-dialog-basic__close {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
color: #111827;
cursor: pointer;
background: white;
border: 0;
border-radius: 9999px;
}
.react-dialog-basic video {
display: block;
width: 100%;
}
@media (prefers-reduced-motion: reduce) {
.react-dialog-basic__backdrop,
.react-dialog-basic__dialog {
transition: none;
}
}
```
## API Reference
### Root
Manages dialog state and provides it to the compound parts.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeOnEscape` | `boolean` | `true` | Whether pressing Escape closes the dialog. |
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled usage. |
| `open` | `boolean` | `false` | Controlled open state. When set, the consumer is responsible for toggling. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the dialog is currently open. |
| `status` | `'idle' \| 'starting' \| 'ending'` | Current phase of the transition lifecycle. |
| `titleId` | `string \| undefined` | Element ID of the dialog title, used for `aria-labelledby`. |
| `descriptionId` | `string \| undefined` | Element ID of the dialog description, used for `aria-describedby`. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Backdrop
Presentational layer behind a dialog while it is rendered, including its exit transition.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Close
Renders a button that closes the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Description
Renders the description announced with the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Popup
Renders the modal dialog while it is open.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Title
Renders the heading that labels the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Trigger
Renders a button that opens the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
---
# AlertDialog
A modal alert dialog for urgent messages that require acknowledgement
## Import
```tsx
import { AlertDialog } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
Use an alert dialog for an urgent message that interrupts the user and requires acknowledgement. Your application controls when it opens and supplies its content.
`AlertDialog.Root` adds alert semantics, while the other `AlertDialog` parts are re-exported from [`Dialog`](https://videojs.org/docs/framework/react/reference/components/dialog). For general modal content, including a player opened from a thumbnail, use `Dialog.Root` instead.
Use `open` with `onOpenChange` for controlled state, or `defaultOpen` for uncontrolled state. `Root` provides state without rendering an element, while `Popup` renders only while the dialog is open.
The close part and Escape close the dialog. The dialog stays rendered during a closing transition, then restores focus to the element that was focused before it opened.
## Styling
Use the open and transition data attributes to style visibility and animations:
| Attribute | Description |
| --- | --- |
| `data-open` | Present while the dialog is open, including its closing transition |
| `data-starting-style` | Present at the start of the opening transition |
| `data-ending-style` | Present during the closing transition |
React renders separate backdrop and popup elements. Add a `className` to each part and style them independently:
```css
.alert-dialog-backdrop[data-starting-style],
.alert-dialog-backdrop[data-ending-style],
.alert-dialog-popup[data-starting-style],
.alert-dialog-popup[data-ending-style] {
opacity: 0;
}
```
## Accessibility
The dialog uses `role="alertdialog"` and `aria-modal="true"`. The title and description receive generated IDs that are connected with `aria-labelledby` and `aria-describedby`. Focus moves into the dialog when it opens, stays within it while open, and returns to the previously focused element when it closes. Content outside the dialog is inert while it is open. Pressing Escape closes the dialog.
## Examples
### Basic usage
**App.tsx**
```tsx
import { AlertDialog } from '@videojs/react';
import { useState } from 'react';
export default function BasicUsage() {
const [open, setOpen] = useState(false);
return (
setOpen(true)}>
Open alert dialog
Stop playback?
Your current playback position will be lost.
Continue
);
}
```
**App.css**
```css
.react-alert-dialog-basic {
position: relative;
display: grid;
place-items: center;
min-height: 240px;
overflow: hidden;
background: #111827;
border-radius: 12px;
}
.react-alert-dialog-basic__trigger,
.react-alert-dialog-basic__close {
padding: 8px 16px;
color: #111827;
cursor: pointer;
background: #fff;
border: 0;
border-radius: 9999px;
}
.react-alert-dialog-basic__backdrop {
position: absolute;
inset: 0;
background: rgb(3 7 18 / 92%);
transition: opacity 150ms ease;
}
.react-alert-dialog-basic__dialog {
position: absolute;
inset: 0;
z-index: 1;
display: grid;
gap: 12px;
place-content: center;
padding: 32px;
color: #fff;
text-align: center;
transition: opacity 150ms ease;
}
.react-alert-dialog-basic__backdrop[data-starting-style],
.react-alert-dialog-basic__backdrop[data-ending-style],
.react-alert-dialog-basic__dialog[data-starting-style],
.react-alert-dialog-basic__dialog[data-ending-style] {
opacity: 0;
}
.react-alert-dialog-basic__title {
font-size: 20px;
font-weight: 600;
}
.react-alert-dialog-basic__description {
color: #d1d5db;
}
.react-alert-dialog-basic__close {
justify-self: center;
}
```
## API Reference
### Root
Manages alert dialog state and provides it to the shared dialog parts.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeOnEscape` | `boolean` | — | Whether pressing Escape closes the dialog. |
| `defaultOpen` | `boolean` | — | Initial open state for uncontrolled usage. |
| `open` | `boolean` | — | Controlled open state. When set, the consumer is responsible for toggling. |
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the dialog is currently open. |
| `status` | `'idle' \| 'starting' \| 'ending'` | Current phase of the transition lifecycle. |
| `titleId` | `string \| undefined` | Element ID of the dialog title, used for `aria-labelledby`. |
| `descriptionId` | `string \| undefined` | Element ID of the dialog description, used for `aria-describedby`. |
### Backdrop
Presentational layer behind a dialog while it is rendered, including its exit transition.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Close
Renders a button that closes the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Description
Renders the description announced with the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Popup
Renders the modal dialog while it is open.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Title
Renders the heading that labels the dialog.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
---
# ErrorDialog
An alert dialog that presents and dismisses playback errors
## Import
```tsx
import { ErrorDialog } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`ErrorDialog` is a specialized root driven by the player’s [error feature](https://videojs.org/docs/framework/react/reference/api/feature-error). It uses alert semantics and reuses the backdrop, popup, title, description, and close parts from [`Dialog`](https://videojs.org/docs/framework/react/reference/components/dialog); it is not nested inside `AlertDialog`. It opens when the attached media reports an error and stays hidden otherwise. Closing it dismisses the current error in the player store.
The title, description, and close label use localized default text. Provide children to any of those parts to replace its default content. Known media error codes use the matching translated message, while custom error messages are shown as written.
The included player skins already render an error dialog. Compose this component directly when building a custom skin.
## Styling
Use `data-open`, `data-starting-style`, and `data-ending-style` to style visibility and transitions.
React renders the backdrop and popup as separate DOM elements. Add a `className` to each part and style their transitions independently:
```css
.error-dialog-backdrop[data-starting-style],
.error-dialog-backdrop[data-ending-style],
.error-dialog-popup[data-starting-style],
.error-dialog-popup[data-ending-style] {
opacity: 0;
}
```
## Accessibility
The popup uses `role="alertdialog"`. Its title and description are connected with `aria-labelledby` and `aria-describedby`.
Modality is scoped to the player container rather than the page. While the error is open, the rest of the container is inert and focus that lands elsewhere inside it returns to the popup, but content outside the player stays interactive and `aria-modal` is omitted. Only when the popup renders outside the container does the dialog fall back to document-wide modality: `aria-modal="true"`, Tab cycling within the popup, and everything outside it inert.
Pressing Escape or activating the close part dismisses the error.
## Examples
The example replaces the video source with invalid local data so the media element reports an error without making a failing network request.
### Basic usage
**App.tsx**
```tsx
import { Container, createPlayer, ErrorDialog } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import { useRef } from 'react';
const { Player } = createPlayer({ features: videoFeatures });
const brokenSource = 'data:video/mp4;base64,AAAA';
export default function BasicUsage() {
const videoRef = useRef(null);
const triggerError = () => {
const video = videoRef.current;
if (!video) return;
video.src = brokenSource;
video.load();
};
return (
Trigger a playback error
);
}
```
**App.css**
```css
.react-error-dialog-basic {
position: relative;
display: block;
overflow: hidden;
border-radius: 12px;
}
.react-error-dialog-basic__video {
display: block;
width: 100%;
}
.react-error-dialog-basic__trigger,
.react-error-dialog-basic__close {
padding: 8px 16px;
color: #111827;
cursor: pointer;
background: #fff;
border: 0;
border-radius: 9999px;
}
.react-error-dialog-basic__trigger {
position: absolute;
bottom: 16px;
left: 16px;
}
.react-error-dialog-basic__backdrop {
position: absolute;
inset: 0;
background: rgb(3 7 18 / 92%);
transition: opacity 150ms ease;
}
.react-error-dialog-basic__dialog {
position: absolute;
inset: 0;
z-index: 1;
display: grid;
gap: 12px;
place-content: center;
padding: 32px;
color: #fff;
text-align: center;
transition: opacity 150ms ease;
}
.react-error-dialog-basic__backdrop[data-starting-style],
.react-error-dialog-basic__backdrop[data-ending-style],
.react-error-dialog-basic__dialog[data-starting-style],
.react-error-dialog-basic__dialog[data-ending-style] {
opacity: 0;
}
.react-error-dialog-basic__title {
font-size: 20px;
font-weight: 600;
}
.react-error-dialog-basic__description {
color: #d1d5db;
}
.react-error-dialog-basic__close {
justify-self: center;
}
```
## API Reference
### Root
Opens from player error state and provides it to the shared dialog parts.
#### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `transitionStarting` | `boolean` | Whether the open transition is in progress. |
| `transitionEnding` | `boolean` | Whether the close transition is in progress. |
| `open` | `boolean` | Whether the dialog is currently open. |
| `status` | `'idle' \| 'starting' \| 'ending'` | Current phase of the transition lifecycle. |
| `titleId` | `string \| undefined` | Element ID of the dialog title, used for `aria-labelledby`. |
| `descriptionId` | `string \| undefined` | Element ID of the dialog description, used for `aria-describedby`. |
### Close
Renders a localized button that closes the dialog and dismisses the player error.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Description
Renders the localized playback error message, or authored children when provided.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Title
Renders the localized error dialog heading, or authored children when provided.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
### Backdrop
Presentational layer behind a dialog while it is rendered, including its exit transition.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
### Popup
Renders the modal dialog while it is open.
#### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string \| ((state: DialogState) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: DialogState) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: DialogState) => CSSProperties \| undefined)` | — | Style or function returning style from state. |
#### Data attributes
| Attribute | Description |
| --- | --- |
| `data-open` | Present when the dialog is open. |
| `data-starting-style` | Present during the open transition. |
| `data-ending-style` | Present during the close transition. |
---
# Gesture
Map tap and double-tap gestures to player actions
## Import
```tsx
import { Gesture } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`Gesture` is a behavior component: it renders no visible interface. Place it inside a [Container](https://videojs.org/docs/framework/react/reference/components/player-container) to map a `tap` or `doubletap` gesture to a player action.
A tap must use the primary pointer and complete within 250 milliseconds. When a matching double-tap gesture exists, the first tap waits briefly to see whether a second tap follows.
Use `pointer` to accept only `mouse`, `touch`, or `pen` input. Use `region` to divide the container horizontally:
- Left and right regions split the container into halves.
- Left, center, and right regions split it into thirds.
- A center-only region covers the full container.
- A single left or right region covers that half of the container.
An unregional gesture acts as a fallback outside the active regions. When several registrations match the same gesture, the first registration wins.
Choose a built-in `action` from the API reference below, or use an application-defined name to call a same-named action on the player store. Pass `value` as seconds for `seekStep` or as a volume delta such as `0.05` or `-0.05` for `volumeStep`. When omitted, these actions use the 10-second or five-percentage-point default. A left-region seek gesture uses the negative step. Unknown action names do nothing and warn in development.
## Accessibility
Gestures supplement visible, keyboard-operable controls; they should never be the only way to perform an action. Gestures ignore pointer activity that begins on interactive controls, links, form fields, menu items, or elements marked with `data-interactive`.
## Examples
### Basic Usage
This example follows the common video-player pattern: click to play or pause, double-click the left or right side to seek ten seconds, and double-click the center to toggle fullscreen.
**App.tsx**
```tsx
import { Container, createPlayer, Gesture, PlayButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
Click: play/pause · Double-click: −10s · fullscreen · +10s
(
{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}
)}
/>
);
}
```
**App.css**
```css
.react-gesture-basic {
position: relative;
}
.react-gesture-basic video {
width: 100%;
}
.react-gesture-basic__instructions {
position: absolute;
top: 10px;
left: 10px;
padding: 6px 10px;
margin: 0;
color: white;
pointer-events: none;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
}
.react-gesture-basic__button {
position: absolute;
bottom: 10px;
left: 10px;
padding: 8px 20px;
color: black;
cursor: pointer;
background: rgb(255 255 255 / 80%);
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 9999px;
}
```
## API Reference
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `action` (required) | `'togglePaused' \| 'toggleMuted' \| 'toggleFullscreen' \| 'toggleSubtitles' \| 'togglePictureInPicture' \| 'toggleControls' \| 'seekStep' \| 'volumeStep' \| 'speedUp' \| 'speedDown' \| string & {}` | — | Built-in player action or same-named custom store action to run when the gesture is recognized. |
| `type` (required) | `'tap' \| 'doubletap'` | — | Gesture to recognize. |
| `disabled` | `boolean` | — | Whether the gesture is disabled. |
| `pointer` | `'mouse' \| 'touch' \| 'pen'` | — | Pointer type that may activate the gesture. All pointer types are accepted when omitted. |
| `region` | `'left' \| 'center' \| 'right'` | — | Optional horizontal part of the container that may activate the gesture. |
| `value` | `number` | — | Numeric value passed to actions such as `seekStep` and `volumeStep`. Uses their shared step when omitted. |
---
# Hotkey
Register a keyboard shortcut that invokes a player action
## Import
```tsx
import { Hotkey } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`Hotkey` is a behavior component: it renders no visible interface. Place it inside a [Container](https://videojs.org/docs/framework/react/reference/components/player-container) to register one key pattern and player action.
By default, the shortcut responds while the player container has focus. Set `target="document"` only when the shortcut should apply across the entire document.
Key patterns are case-insensitive and may use:
- Named keys such as `Space`, `ArrowLeft`, and `ArrowRight`.
- Exact modifiers such as `Ctrl+k`, `Shift+f`, `Alt+m`, and `Meta+k`.
- `Mod` for `Meta` on macOS and `Ctrl` elsewhere.
- The special `0-9` range for digit-based seeking.
Choose an `action` from the API reference below. Pass `value` as seconds for `seekStep`, a volume delta such as `0.05` or `-0.05` for `volumeStep`, or a percentage from 0 to 100 for `seekToPercent`. When omitted, `seekStep` defaults to 10 seconds and `volumeStep` defaults to five percentage points; `ArrowLeft`, `j`, and `ArrowDown` use the negative step. When `value` is omitted from `seekToPercent` with `keys="0-9"`, the pressed digit supplies the percentage.
Toggle actions ignore held-key repeats. Step, rate, and percentage actions may repeat. A matched shortcut prevents the browser’s default behavior. Unmodified shortcuts are ignored in editable fields, and Space or Enter still activates focused buttons, links, and sliders normally.
## Accessibility
Hotkeys supplement operable controls; they should never be the only way to perform an action. Built-in buttons use matching hotkey registrations to expose `aria-keyshortcuts`, and their tooltips can display the registered shortcut.
## Examples
### Basic Usage
This player uses Space to play or pause, M to mute, and the arrow keys to seek by five seconds. Click the player first to focus its container.
**App.tsx**
```tsx
import { Container, createPlayer, Hotkey, PlayButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
Space: play/pause · M: mute · ←/→: seek
(
{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}
)}
/>
);
}
```
**App.css**
```css
.react-hotkey-basic {
position: relative;
}
.react-hotkey-basic video {
width: 100%;
}
.react-hotkey-basic__instructions {
position: absolute;
top: 10px;
left: 10px;
padding: 6px 10px;
margin: 0;
color: white;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
}
.react-hotkey-basic__button {
position: absolute;
bottom: 10px;
left: 10px;
padding: 8px 20px;
color: black;
cursor: pointer;
background: rgb(255 255 255 / 80%);
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 9999px;
}
```
## API Reference
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `action` (required) | `'togglePaused' \| 'toggleMuted' \| 'toggleFullscreen' \| 'toggleSubtitles' \| 'togglePictureInPicture' \| 'seekStep' \| 'volumeStep' \| 'speedUp' \| 'speedDown' \| 'seekToPercent'` | — | Player action to run when the key pattern matches. |
| `keys` (required) | `string` | — | Key pattern to match, such as `Space`, `ArrowRight`, or `Mod+k`. |
| `disabled` | `boolean` | — | Whether the hotkey is disabled. |
| `target` | `'player' \| 'document'` | — | Whether to listen on the player container or the document. |
| `value` | `number` | — | Numeric value passed to actions such as `seekStep`, `volumeStep`, and `seekToPercent`. Arrow-key seek and volume actions use their shared input-action step when omitted. |
---
# StatusAnnouncer
Announce player state changes through a persistent status live region
## Import
```tsx
import { StatusAnnouncer } from '@videojs/react';
```
## Anatomy
```tsx
```
## Behavior
`StatusAnnouncer` watches player-store snapshots, so it reports changes made by buttons, sliders, custom controls, or direct player actions. It does not depend on [`Hotkey`](https://videojs.org/docs/framework/react/reference/components/hotkey) or [`Gesture`](https://videojs.org/docs/framework/react/reference/components/gesture) events.
The first snapshot establishes a baseline without announcing it. Later changes are handled in two groups:
| Timing | Changes |
| --- | --- |
| Immediate | Playing or paused, captions on or off, fullscreen entered or exited, picture-in-picture entered or exited, and playback rate |
| Debounced by 200 milliseconds | The final volume or mute value and the final time after a completed seek |
Regular playback-time updates are ignored. A seek announcement is queued only after `seeking` changes from `true` to `false` at a different time. When several immediate states change in one snapshot, their translated labels are combined into one announcement.
The component always renders with `role="status"`. Its `[data-status-announcer-content]` child is replaced for every announcement, including repeated text, so assistive technology receives a fresh live-region change. After `closeDelay`, which defaults to 800 milliseconds, the label is cleared while the status region remains rendered.
Announcements use the active Video.js locale and translations.
Use the `labels` prop to override individual translated labels for one announcer.
## Styling
`StatusAnnouncer` is not visually hidden by the primitive. Apply a visually-hidden class while keeping it in the accessibility tree. Do not use `display: none`, `visibility: hidden`, or the `hidden` attribute.
Add a `className` to the component:
```css
.status-announcer {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
}
```
## Accessibility
The `status` role provides implicit polite live-region behavior, so the component does not add an explicit `aria-live` attribute.
Keep one announcer inside each player `Container` whose state changes should be reported.
Debounced volume and completed-seek announcements are suppressed while an element with `role="slider"` inside the same container has focus. The focused slider provides its own value feedback, and suppressing the separate status message avoids duplicate announcements. A focused slider outside that container does not suppress it.
Use [`SeekIndicator`](https://videojs.org/docs/framework/react/reference/components/seek-indicator) for temporary visual seek feedback. Keep that visual value out of a live region; `StatusAnnouncer` owns the accessible announcement.
## Examples
### Basic Usage
Use the playback button for an immediate announcement and the mute button for a debounced announcement. The sliders provide their own focused value feedback, so `StatusAnnouncer` suppresses its duplicate volume and completed-seek message.
**App.tsx**
```tsx
import {
Container,
createPlayer,
MuteButton,
PlayButton,
StatusAnnouncer,
TimeSlider,
VolumeSlider,
} from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';
import './BasicUsage.css';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
These controls announce status changes to screen readers.
(
{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}
)}
/>
{state.muted ? 'Unmute' : 'Mute'} }
/>
);
}
```
**App.css**
```css
.react-status-announcer-basic {
position: relative;
}
.react-status-announcer-basic video {
display: block;
width: 100%;
}
.react-status-announcer-basic__instructions {
position: absolute;
top: 10px;
left: 10px;
padding: 6px 10px;
margin: 0;
color: white;
background: rgb(0 0 0 / 70%);
border-radius: 4px;
}
.react-status-announcer-basic__controls {
position: absolute;
right: 12px;
bottom: 36px;
left: 12px;
display: flex;
gap: 8px;
align-items: center;
}
.react-status-announcer-basic__button {
padding: 7px 14px;
color: black;
cursor: pointer;
background: rgb(255 255 255 / 85%);
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 9999px;
}
.react-status-announcer-basic__volume-slider,
.react-status-announcer-basic__time-slider {
position: relative;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.react-status-announcer-basic__volume-slider {
width: 100px;
}
.react-status-announcer-basic__time-slider {
position: absolute;
right: 12px;
bottom: 8px;
left: 12px;
}
.react-status-announcer-basic__track {
position: absolute;
right: 0;
left: 0;
height: 5px;
overflow: hidden;
background: rgb(255 255 255 / 30%);
border-radius: 9999px;
}
.react-status-announcer-basic__buffer,
.react-status-announcer-basic__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: inherit;
}
.react-status-announcer-basic__buffer {
width: var(--media-slider-buffer);
background: rgb(255 255 255 / 30%);
}
.react-status-announcer-basic__fill {
width: var(--media-slider-fill);
background: white;
}
.react-status-announcer-basic__thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 3px rgb(0 0 0 / 40%);
translate: -50%;
}
.react-status-announcer-basic__announcer {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
}
```
## API Reference
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeDelay` | `number` | — | Delay in milliseconds before the current announcement label is cleared. |
| `labels` | `Partial` | — | Overrides for the translated announcement labels. |
| `shouldAnnounce` | `(() => boolean)` | — | Whether debounced seek and volume changes should be announced. |
### State
State is accessible via the `render`, `className`, and `style` props.
| Property | Type | Description |
| --- | --- | --- |
| `generation` | `number` | Increments for each announcement so repeated labels replace the live-region content. |
| `label` | `string \| null` | Current announcement text, or `null` when the live region is empty. |
---
# Icons
The SVG icon sets used by the packaged skins, as React components, SVG strings, and the media-icon element
The icons the packaged skins render. Skin source added with the [Shadcn registry](https://videojs.org/docs/guides/installation/shadcn?framework=react) imports its icons from these modules, and your own controls can use them too.
Each icon is a React component that renders an inline ``.
## Import
```tsx
import { PlayIcon } from "@videojs/react/icons";
```
## Icon sets
Three sets ship with the same icon names and different designs.
| Set | Module | Used by |
| --- | --- | --- |
| Default | `@videojs/react/icons` | The default skins, such as [`VideoSkin`](https://videojs.org/docs/framework/react/reference/components/video-skin) |
| Neutral | `@videojs/react/icons/neutral` | The neutral skins, such as [`NeutralVideoSkin`](https://videojs.org/docs/framework/react/reference/components/video-neutral-skin) |
| Compat | `@videojs/react/icons/compat` | The compat skins, such as [`CompatVideoSkin`](https://videojs.org/docs/framework/react/reference/components/video-compat-skin) |
## Available icons
| Export |
| --- |
| `AirPlayEnterIcon` |
| `AirPlayExitIcon` |
| `CaptionsOffIcon` |
| `CaptionsOnIcon` |
| `CastEnterIcon` |
| `CastExitIcon` |
| `CheckIcon` |
| `ChevronIcon` |
| `FullscreenEnterIcon` |
| `FullscreenExitIcon` |
| `GearIcon` |
| `PauseIcon` |
| `PipEnterIcon` |
| `PipExitIcon` |
| `PlayIcon` |
| `QualityIcon` |
| `RestartIcon` |
| `SeekIcon` |
| `SpeechIcon` |
| `SpeedIcon` |
| `SpinnerIcon` |
| `SwitchesIcon` |
| `VolumeHighIcon` |
| `VolumeLowIcon` |
| `VolumeOffIcon` |
## Props
Icons accept `IconProps`, the standard SVG element props (`SVGProps`), and forward their ref to the ``. Props you pass override the defaults:
| Attribute | Default |
| --- | --- |
| `width`, `height` | `18` |
| `viewBox` | `0 0 18 18` |
| `fill` | `currentColor` |
| `aria-hidden` | `true` |
```tsx
import { PlayButton } from "@videojs/react";
import { PauseIcon, PlayIcon } from "@videojs/react/icons";
(
{state.paused ? : }
)}
/>
```
## Styling
Icons use `fill="currentColor"`, so they take the text color of their parent. They are drawn on an 18×18 grid and render most sharply at 18px or a whole multiple, such as 36px.
```css
.icon {
width: 36px;
height: 36px;
color: white;
}
```
## Accessibility
Every icon renders with `aria-hidden="true"`. Give the surrounding control its accessible name. Built-in buttons, such as the [play button](https://videojs.org/docs/framework/react/reference/components/play-button), provide one automatically.