VolumePopover
A volume-aware popover that keeps mute available when volume level controls are unavailable
Import
import { VolumePopover } from '@videojs/react';import '@videojs/html/ui/volume-popover';Anatomy
<VolumePopover.Root>
<VolumePopover.Trigger render={<MuteButton />} />
<VolumePopover.Popup>
<VolumeSlider.Root />
</VolumePopover.Popup>
</VolumePopover.Root><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
VolumePopover combines popover behavior 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 HTML 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. HTML also receives native hidden and data-hidden when it cannot.
media-volume-popover[data-open] {
opacity: 1;
}React renders the popup as a standard <div>. Add a className to select it:
.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
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>
);
}
.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%);
}
<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>
.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%);
}
import '@videojs/html/video/player';
import '@videojs/html/ui/mute-button';
import '@videojs/html/ui/volume-popover';
import '@videojs/html/ui/volume-slider';
API Reference
Rootmedia-volume-popover
Owns volume availability and the popover interaction lifecycle.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
align | 'start' | 'center' | 'end' | — | |
| |||
closeDelay | number | — | |
| |||
closeOnEscape | boolean | — | |
| |||
closeOnOutsideClick | boolean | — | |
| |||
defaultOpen | boolean | — | |
| |||
delay | number | — | |
| |||
modal | boolean | 'trap-focus' | — | |
| |||
open | boolean | — | |
| |||
openOnHover | boolean | — | |
| |||
side | 'top' | 'bottom' | 'left' | 'right' | — | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
transitionStarting | boolean | |
| ||
transitionEnding | boolean | |
| ||
open | boolean | |
status | 'idle' | 'starting' | 'ending' | |
side | 'top' | 'bottom' | 'left' | 'right' | |
| ||
align | 'start' | 'center' | 'end' | |
modal | boolean | 'trap-focus' | |
availability | MediaFeatureAvailability | |
| ||
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-open | ||
| ||
data-side | 'top' | 'bottom' | 'left' | 'right' | |
| ||
data-align | 'start' | 'center' | 'end' | |
| ||
data-starting-style | ||
| ||
data-ending-style | ||
| ||
data-availability | MediaFeatureAvailability | |
| ||
Events
| Event | Description |
|---|---|
open-change | Fired when the popover's open state changes. |
PopupPopup
Positioned volume content. Omitted when volume level controls are unavailable.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: PopoverState) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: PopoverState) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: PopoverState) => CSSProperties | undefined) | — | |
| |||
TriggerTrigger
Opens the volume popup, or renders its mute-button fallback when volume level controls are unavailable.
Props
| Prop | Type | Default | Details |
|---|---|---|---|
className | string | ((state: PopoverState) => string | undefined) | — | |
| |||
render | ReactElement | ((props: HTMLProps, state: PopoverState) => ReactElement | null) | — | |
| |||
style | CSSProperties | ((state: PopoverState) => CSSProperties | undefined) | — | |
| |||