# media-volume-popover

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

## Import

```ts
import '@videojs/html/ui/volume-popover';
import '@videojs/html/ui/mute-button';
import '@videojs/html/ui/volume-slider';
import '@videojs/html/ui/slider-track';
import '@videojs/html/ui/slider-fill';
import '@videojs/html/ui/slider-thumb';
```

## Anatomy

```html
<media-mute-button commandfor="volume-popup"></media-mute-button>
<media-volume-popover id="volume-popup">
  <media-volume-slider></media-volume-slider>
</media-volume-popover>
```

## Behavior

`<media-volume-popover>` combines [popover behavior](https://videojs.org/docs/framework/html/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.

The trigger can target the popup with `commandfor`. An immediately adjacent trigger is linked automatically when neither element has an authored ID.

## Styling

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

When it cannot, the element also receives native `hidden` and `data-hidden`.

```css
media-volume-popover[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

**index.html**

```html
<video-player class="html-volume-popover-basic">
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop></video>
    <div class="html-volume-popover-basic__controls">
      <media-mute-button commandfor="volume-popover-demo" class="html-volume-popover-basic__trigger">
        <span class="muted">Unmute</span>
        <span class="unmuted">Mute</span>
      </media-mute-button>
      <media-volume-popover id="volume-popover-demo" open-on-hover side="top" class="html-volume-popover-basic__popup">
        <media-volume-slider orientation="vertical" class="html-volume-popover-basic__slider">
          <media-slider-track class="html-volume-popover-basic__track">
            <media-slider-fill class="html-volume-popover-basic__fill"></media-slider-fill>
          </media-slider-track>
          <media-slider-thumb class="html-volume-popover-basic__thumb"></media-slider-thumb>
        </media-volume-slider>
      </media-volume-popover>
    </div>
  </media-container>
</video-player>
```

**index.css**

```css
.html-volume-popover-basic media-container {
  position: relative;
  display: block;
}

.html-volume-popover-basic video {
  width: 100%;
}

.html-volume-popover-basic__controls {
  position: absolute;
  bottom: 10px;
  left: 10px;
}

.html-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);
}

.html-volume-popover-basic__trigger .muted,
.html-volume-popover-basic__trigger .unmuted {
  display: none;
}

.html-volume-popover-basic__trigger[data-muted] .muted,
.html-volume-popover-basic__trigger:not([data-muted]) .unmuted {
  display: inline;
}

.html-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;
}

.html-volume-popover-basic__slider {
  position: relative;
  display: block;
  width: 16px;
  height: 96px;
  margin: auto;
  cursor: pointer;
}

.html-volume-popover-basic__track {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 6px;
  width: 4px;
  background: rgb(255 255 255 / 30%);
  border-radius: 9999px;
}

.html-volume-popover-basic__fill {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: var(--media-slider-fill);
  background: white;
  border-radius: inherit;
}

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

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/mute-button';
import '@videojs/html/ui/volume-popover';
import '@videojs/html/ui/volume-slider';
import '@videojs/html/ui/slider-track';
import '@videojs/html/ui/slider-fill';
import '@videojs/html/ui/slider-thumb';
```

## API Reference

### media-volume-popover

A volume-aware popover that keeps its adjacent mute trigger available as a fallback.

#### 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` (attribute `close-delay`) | `number` | — | Delay in ms before closing after pointer leaves. |
| `closeOnEscape` (attribute `close-on-escape`) | `boolean` | — | Close the popup when the Escape key is pressed. |
| `closeOnOutsideClick` (attribute `close-on-outside-click`) | `boolean` | — | Close the popup when clicking outside the trigger and popup. |
| `defaultOpen` (attribute `default-open`) | `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` (attribute `open-on-hover`) | `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 reflected as data attributes for CSS styling.

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

#### Events

| Event | Description |
| --- | --- |
| `open-change` | Fired when the popover's open state changes. |

---

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