StatusAnnouncer
Announce player state changes through a persistent status live region
Import
import { StatusAnnouncer } from '@videojs/react';import '@videojs/html/ui/status-announcer';Anatomy
<StatusAnnouncer /><media-status-announcer></media-status-announcer>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 or 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.
The custom element receives labels from the nearest player i18n context.
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.
media-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%);
}Add a className to the component:
.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 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.
These controls announce status changes to screen readers.
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 (
<Player>
<Container className="react-status-announcer-basic">
<Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline />
<p className="react-status-announcer-basic__instructions">
These controls announce status changes to screen readers.
</p>
<div className="react-status-announcer-basic__controls">
<PlayButton
className="react-status-announcer-basic__button"
render={(props, state) => (
<button {...props}>{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}</button>
)}
/>
<MuteButton
className="react-status-announcer-basic__button"
render={(props, state) => <button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>}
/>
<VolumeSlider.Root className="react-status-announcer-basic__volume-slider">
<VolumeSlider.Track className="react-status-announcer-basic__track">
<VolumeSlider.Fill className="react-status-announcer-basic__fill" />
</VolumeSlider.Track>
<VolumeSlider.Thumb className="react-status-announcer-basic__thumb" />
</VolumeSlider.Root>
</div>
<TimeSlider.Root className="react-status-announcer-basic__time-slider">
<TimeSlider.Track className="react-status-announcer-basic__track">
<TimeSlider.Buffer className="react-status-announcer-basic__buffer" />
<TimeSlider.Fill className="react-status-announcer-basic__fill" />
</TimeSlider.Track>
<TimeSlider.Thumb className="react-status-announcer-basic__thumb" />
</TimeSlider.Root>
<StatusAnnouncer className="react-status-announcer-basic__announcer" />
</Container>
</Player>
);
}
.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%);
}
These controls announce status changes to screen readers.
<video-player>
<media-container class="html-status-announcer-basic">
<video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline></video>
<p class="html-status-announcer-basic__instructions">These controls announce status changes to screen readers.</p>
<div class="html-status-announcer-basic__controls">
<media-play-button class="html-status-announcer-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
<span class="show-when-ended">Replay</span>
</media-play-button>
<media-mute-button class="html-status-announcer-basic__button">
<span class="show-when-muted">Unmute</span>
<span class="show-when-unmuted">Mute</span>
</media-mute-button>
<media-volume-slider class="html-status-announcer-basic__volume-slider">
<media-slider-track class="html-status-announcer-basic__track">
<media-slider-fill class="html-status-announcer-basic__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="html-status-announcer-basic__thumb"></media-slider-thumb>
</media-volume-slider>
</div>
<media-time-slider class="html-status-announcer-basic__time-slider">
<media-slider-track class="html-status-announcer-basic__track">
<media-slider-buffer class="html-status-announcer-basic__buffer"></media-slider-buffer>
<media-slider-fill class="html-status-announcer-basic__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="html-status-announcer-basic__thumb"></media-slider-thumb>
</media-time-slider>
<media-status-announcer class="html-status-announcer-basic__announcer"></media-status-announcer>
</media-container>
</video-player>
.html-status-announcer-basic {
position: relative;
}
.html-status-announcer-basic video {
display: block;
width: 100%;
}
.html-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;
}
.html-status-announcer-basic__controls {
position: absolute;
right: 12px;
bottom: 36px;
left: 12px;
display: flex;
gap: 8px;
align-items: center;
}
.html-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;
}
.html-status-announcer-basic__button .show-when-paused,
.html-status-announcer-basic__button .show-when-playing,
.html-status-announcer-basic__button .show-when-ended,
.html-status-announcer-basic__button .show-when-muted,
.html-status-announcer-basic__button .show-when-unmuted {
display: none;
}
.html-status-announcer-basic__button[data-paused]:not([data-ended]) .show-when-paused,
.html-status-announcer-basic__button:not([data-paused]) .show-when-playing,
.html-status-announcer-basic__button[data-ended] .show-when-ended,
.html-status-announcer-basic__button[data-muted] .show-when-muted,
.html-status-announcer-basic__button:not([data-muted]) .show-when-unmuted {
display: inline;
}
.html-status-announcer-basic__volume-slider,
.html-status-announcer-basic__time-slider {
position: relative;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.html-status-announcer-basic__volume-slider {
width: 100px;
}
.html-status-announcer-basic__time-slider {
position: absolute;
right: 12px;
bottom: 8px;
left: 12px;
}
.html-status-announcer-basic__track {
position: absolute;
right: 0;
left: 0;
height: 5px;
overflow: hidden;
background: rgb(255 255 255 / 30%);
border-radius: 9999px;
}
.html-status-announcer-basic__buffer,
.html-status-announcer-basic__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: inherit;
}
.html-status-announcer-basic__buffer {
width: var(--media-slider-buffer);
background: rgb(255 255 255 / 30%);
}
.html-status-announcer-basic__fill {
width: var(--media-slider-fill);
background: white;
}
.html-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%;
}
.html-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%);
}
import '@videojs/html/video/player';
import '@videojs/html/ui/mute-button';
import '@videojs/html/ui/play-button';
import '@videojs/html/ui/status-announcer';
import '@videojs/html/ui/time-slider';
import '@videojs/html/ui/volume-slider';
API Reference
Props
| Prop | Type | Default | Details |
|---|---|---|---|
closeDelay | number | — | |
| |||
labels | Partial<StatusAnnouncerLabels> | — | |
| |||
shouldAnnounce | function | — | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
generation | number | |
| ||
label | string | null | |
| ||