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
<QualityRadioGroup
renderItem={(props, item) => (
<Menu.RadioItem {...props}>
{item.label}
<Menu.ItemIndicator checked={item.checked} />
</Menu.RadioItem>
)}
/><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.
QualityRadioGroup uses useQualityOptions to own selection and generate each item. Its required renderItem callback renders the Menu.RadioItem root and receives item state containing the translated label, optional tier and badge, and current checked value. It returns null when the quality feature is not configured.
<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-hidden |
Present / absent | Present when multiple renditions are unavailable. |
data-availability |
"available" / "unavailable" |
Whether multiple renditions are available. |
Unavailable groups receive the native hidden attribute.
Accessibility
The group uses the menu radio group pattern.
The component receives an accessible label from the label prop or defaults to Quality. Override it with aria-label or aria-labelledby.
The element receives an accessible label from the label property or defaults to Quality.
Examples
Basic usage
import { Container, createPlayer, Menu, QualityRadioGroup } 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 {
return (
<Menu.Root side="top" align="end">
<Menu.Trigger className="settings-trigger" render={<button type="button" />}>
Quality
</Menu.Trigger>
<Menu.Content className="menu">
<QualityRadioGroup
className="menu-group"
renderItem={(props, item) => (
<Menu.RadioItem {...props} className="menu-item">
<span>
{item.label}
{item.tier ? <sup className="menu-tier">{item.tier}</sup> : null}
</span>
{item.badge ? <span className="menu-badge">{item.badge}</span> : null}
<Menu.ItemIndicator checked={item.checked} forceMount className="menu-indicator">
✓
</Menu.ItemIndicator>
</Menu.RadioItem>
)}
/>
</Menu.Content>
</Menu.Root>
);
}
export default function BasicUsage() {
return (
<Player>
<Container className="media-container">
<HlsJsVideo src={src} autoPlay crossOrigin="anonymous" muted playsInline loop />
<div className="menu-bar">
<QualityMenu />
</div>
</Container>
</Player>
);
}
.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 | Text) | function | formatRenditionLabel | |
| |||
label | Text | string | Text) | function | '' | |
| |||
State
render, className, and style props.| Property | Type | Details |
|---|---|---|
value | string | |
| ||
options | QualityRadioGroupOption[] | |
| ||
disabled | boolean | |
| ||
availability | 'available' | 'unavailable' | |
| ||
label | Text | string | |
Data attributes
| Attribute | Type | Details |
|---|---|---|
data-quality | string | |
| ||
data-disabled | ||
| ||
data-availability | 'available' | 'unavailable' | |
| ||