# StatusAnnouncer

Announce player state changes through a persistent status live region

## Import

```tsx
import { StatusAnnouncer } from '@videojs/react';
```

## Anatomy

```tsx
<StatusAnnouncer />
```

## 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 (
    <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>
  );
}
```

**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<StatusAnnouncerLabels>` | — | 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. |

---

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