# VolumeIndicator

Display temporary visual feedback for keyboard and gesture volume actions

## Import

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

## Anatomy

```tsx
<VolumeIndicator.Root>
  <VolumeIndicator.Fill>
    <VolumeIndicator.Value />
  </VolumeIndicator.Fill>
</VolumeIndicator.Root>
```

## 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 (
    <Player>
      <Container className="react-volume-indicator-basic" tabIndex={0}>
        <Video
          ref={(video) => {
            if (video) video.volume = 0.5;
          }}
          src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
          autoPlay
          muted
          playsInline
          loop
        />
        <p className="react-volume-indicator-basic__instructions">Focus the player · M: mute · ↑/↓: volume ±5%</p>
        <VolumeIndicator.Root className="react-volume-indicator-basic__indicator" aria-hidden="true">
          <VolumeIndicator.Fill className="react-volume-indicator-basic__fill">
            <VolumeIndicator.Value className="react-volume-indicator-basic__value" />
          </VolumeIndicator.Fill>
        </VolumeIndicator.Root>
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
      </Container>
    </Player>
  );
}
```

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

---

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