# media-status-announcer

Announce player state changes through a persistent status live region

## Import

```ts
import '@videojs/html/ui/status-announcer';
```

## Anatomy

```html
<media-status-announcer></media-status-announcer>
```

## Behavior

`<media-status-announcer>` watches player-store snapshots, so it reports changes made by buttons, sliders, custom controls, or direct player actions. It does not depend on [`<media-hotkey>`](https://videojs.org/docs/framework/html/reference/components/hotkey) or [`<media-gesture>`](https://videojs.org/docs/framework/html/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.

The custom element receives labels from the nearest player i18n context.

## Styling

`<media-status-announcer>` 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.

```css
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%);
}
```

## 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 `<media-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 [`<media-seek-indicator>`](https://videojs.org/docs/framework/html/reference/components/seek-indicator) for temporary visual seek feedback. Keep that visual value out of a live region; `<media-status-announcer>` 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 `<media-status-announcer>` suppresses its duplicate volume and completed-seek message.

**index.html**

```html
<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>
```

**index.css**

```css
.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%);
}
```

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
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/slider-track';
import '@videojs/html/ui/slider-buffer';
import '@videojs/html/ui/slider-fill';
import '@videojs/html/ui/slider-thumb';
import '@videojs/html/ui/volume-slider';
```

## API Reference

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeDelay` (attribute `close-delay`) | `number` | — | Delay in milliseconds before the current announcement label is cleared. |

### State

State is reflected as data attributes for CSS styling.

| 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. |

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
