# VolumePopover

A volume-aware popover that keeps mute available when volume level controls are unavailable

## Import

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

## Anatomy

```tsx
<VolumePopover.Root>
  <VolumePopover.Trigger render={<MuteButton />} />
  <VolumePopover.Popup>
    <VolumeSlider.Root />
  </VolumePopover.Popup>
</VolumePopover.Root>
```

## Behavior

`VolumePopover` combines [popover behavior](https://videojs.org/docs/framework/react/reference/components/popover) with volume availability. When volume level controls are unavailable, it closes and omits the popup while keeping the mute trigger rendered. This lets media with fixed volume keep mute control without exposing a nonfunctional slider.

## Styling

The popup reflects `data-availability="available"` while it can render volume controls.

React renders the popup as a standard `<div>`. 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 (
    <Player>
      <Container className="react-volume-popover-basic">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <div className="react-volume-popover-basic__controls">
          <VolumePopover.Root openOnHover side="top">
            <VolumePopover.Trigger
              className="react-volume-popover-basic__trigger"
              render={
                <MuteButton render={(props, state) => <button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>} />
              }
            />
            <VolumePopover.Popup className="react-volume-popover-basic__popup">
              <VolumeSlider.Root orientation="vertical" className="react-volume-popover-basic__slider">
                <VolumeSlider.Track className="react-volume-popover-basic__track">
                  <VolumeSlider.Fill className="react-volume-popover-basic__fill" />
                </VolumeSlider.Track>
                <VolumeSlider.Thumb className="react-volume-popover-basic__thumb" />
              </VolumeSlider.Root>
            </VolumePopover.Popup>
          </VolumePopover.Root>
        </div>
      </Container>
    </Player>
  );
}
```

**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.<br>- `true`: modal; sets `aria-modal="true"` on the popup.<br>- `'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. |

---

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