media-quality-radio-group
A menu radio group for selecting video quality
Creates radio items from the player video rendition state and selects automatic or manual quality.
Anatomy
<Menu.RadioGroup>
<Menu.GroupLabel />
<Menu.RadioItem>
<Menu.ItemIndicator />
</Menu.RadioItem>
</Menu.RadioGroup>
<media-quality-radio-group>
<template>
<media-menu-radio-item>
<span data-part="label"></span>
<span data-part="tier"></span>
<span data-part="badge"></span>
<media-menu-item-indicator></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-quality-radio-group>
Behavior
The group is available when the configured media exposes more than one video rendition. The first generated option is Auto; selecting it returns control to adaptive bitrate selection. Pass formatRendition to customize visible rendition labels.
useQualityOptions returns null when the quality feature is not configured. Use the returned options with Menu.RadioGroup.
<media-quality-radio-group> generates <media-menu-radio-item> children. Add an optional <template> with a single <media-menu-radio-item> to customize each generated item.
Styling
| Attribute | Values | Description |
|---|---|---|
data-quality
|
string
|
Current quality value. |
data-disabled
|
Present / absent | Present when quality selection is disabled. |
data-availability
|
"available" / "unavailable"
|
Whether multiple renditions are available. |
media-quality-radio-group[data-availability="unavailable"] {
display: none;
}
Accessibility
The group uses the menu radio group pattern.
Give Menu.RadioGroup an accessible name with aria-label or a nested Menu.GroupLabel.
The element receives an accessible label from the label property or defaults to Quality.
Examples
Basic usage
import { createPlayer, Menu, useQualityOptions } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';
const Player = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8';
function QualityMenu(): ReactNode {
const quality = useQualityOptions();
if (quality?.state.availability !== 'available') return null;
return (
<Menu.Root side="top" align="end">
<Menu.Trigger className="settings-trigger" render={<button type="button" />}>
Quality
</Menu.Trigger>
<Menu.Content className="menu">
<Menu.RadioGroup
className="menu-group"
value={quality.value}
onValueChange={quality.setValue}
aria-label="Quality"
>
{quality.options.map((option) => (
<Menu.RadioItem key={option.value} value={option.value} disabled={option.disabled} className="menu-item">
<span>
{option.label}
{option.tier ? <sup className="menu-tier">{option.tier}</sup> : null}
</span>
{option.badge ? <span className="menu-badge">{option.badge}</span> : null}
<Menu.ItemIndicator checked={option.value === quality.value} forceMount className="menu-indicator">
✓
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
);
}
export default function BasicUsage() {
return (
<Player.Provider>
<Player.Container className="media-container">
<HlsJsVideo src={src} autoPlay crossOrigin="anonymous" muted playsInline loop />
<div className="menu-bar">
<QualityMenu />
</div>
</Player.Container>
</Player.Provider>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu {
--media-menu-side-offset: 8px;
box-sizing: border-box;
display: grid;
gap: 2px;
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
margin: 0;
overflow: auto;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border: 0;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-item {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
.menu-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
[role="menuitemradio"][aria-checked="true"] .menu-indicator {
opacity: 1;
}
<video-player class="video-player">
<media-container>
<hlsjs-video
class="video-media"
src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8"
autoplay
crossorigin="anonymous"
muted
playsinline
loop
></hlsjs-video>
<div class="menu-bar">
<button type="button" commandfor="quality-menu" class="settings-trigger">Quality</button>
<media-menu id="quality-menu" side="top" align="end" class="menu">
<media-quality-radio-group class="menu-group" label="Quality">
<template>
<media-menu-radio-item class="menu-item">
<span>
<span data-part="label"></span>
<sup data-part="tier" class="menu-tier"></sup>
</span>
<span data-part="badge" class="menu-badge"></span>
<media-menu-item-indicator force-mount class="menu-indicator">✓</media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-quality-radio-group>
</media-menu>
</div>
</media-container>
</video-player>
.video-player,
.video-player media-container {
position: relative;
display: block;
}
.video-media {
width: 100%;
}
.menu-bar {
position: absolute;
right: 10px;
bottom: 10px;
}
.settings-trigger {
padding: 6px 16px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.menu {
--media-menu-side-offset: 8px;
box-sizing: border-box;
display: grid;
gap: 2px;
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
overflow: auto;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-item {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
min-height: 32px;
padding: 0 10px;
font: inherit;
color: inherit;
cursor: pointer;
background: none;
border: 0;
border-radius: 6px;
}
.menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
.menu-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
media-menu-radio-item[aria-checked="true"] .menu-indicator {
opacity: 1;
}
import '@videojs/html/video/player';
import '@videojs/html/media/hlsjs-video';
import '@videojs/html/ui/menu';
import '@videojs/html/ui/quality-radio-group';
API Reference
Props
| Prop | Type | Default | Details |
|---|---|---|---|
disabled
|
boolean
|
false
|
|
|
|||
formatRendition
|
function
|
formatRenditionLabel
|
|
|
|||
label
|
string | function
|
''
|
|
|
|||
State
render, className, and style props.
| Property | Type | Details |
|---|---|---|
renditions
|
QualityRadioGroupRendition[]
|
|
autoLabel
|
string
|
|
value
|
string
|
|
disabled
|
boolean
|
|
availability
|
'available' | 'unavailable'
|
|
label
|
string
|
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-quality
|
string
|
|
|
||
data-disabled
|
||
|
||
data-availability
|
'available' | 'unavailable'
|
|
|
||