docs(site): add menu reference (#1673)

This commit is contained in:
Sam Potts
2026-07-07 13:06:19 -05:00
committed by GitHub
parent e060cf0d54
commit cc020b4b89
14 changed files with 1082 additions and 2 deletions
@@ -0,0 +1,9 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
---
<HtmlDemo html={html} />
<script>
import "./BasicUsage.ts";
</script>
@@ -0,0 +1,169 @@
.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;
--menu-transition-duration: 220ms;
position: relative;
box-sizing: border-box;
width: var(--media-menu-width);
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
height: var(--media-menu-height);
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
overflow: hidden;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border-radius: 8px;
backdrop-filter: blur(10px);
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: width, height, opacity, filter;
}
.menu-panel {
position: absolute;
inset: 0;
display: grid;
gap: 2px;
padding: 6px;
overflow: auto;
overscroll-behavior: none;
outline: none;
translate: 0 0;
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: translate, filter;
will-change: translate;
}
.menu-panel[data-starting-style],
.menu-panel[data-ending-style] {
overflow: hidden;
}
.menu-panel[data-menu-root-view][data-menu-view-state="inactive"] {
filter: blur(8px);
translate: -100% 0;
}
.menu-panel[data-submenu] {
z-index: 1;
}
.menu-panel[data-submenu]:not([data-open], [data-ending-style]) {
translate: -100% 0;
transition-property: none;
}
.menu-panel[data-submenu][data-starting-style],
.menu-panel[data-submenu][data-ending-style] {
pointer-events: none;
filter: blur(8px);
}
.menu-panel[data-submenu][data-starting-style][data-direction="forward"],
.menu-panel[data-submenu][data-ending-style][data-direction="back"] {
translate: 100% 0;
}
.menu-panel[data-submenu][data-ending-style][data-direction="forward"],
.menu-panel[data-submenu][data-starting-style][data-direction="back"] {
translate: -100% 0;
}
.menu-item,
.menu-back {
display: flex;
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],
.menu-back:hover {
background: rgba(255, 255, 255, 0.16);
}
.menu-back {
gap: 8px;
justify-content: flex-start;
}
.menu-item[data-availability="unavailable"] {
display: none;
}
.menu-value {
display: inline-flex;
gap: 8px;
align-items: center;
color: rgba(255, 255, 255, 0.72);
}
.menu-value [aria-hidden] {
font-size: 18px;
line-height: 1;
}
.menu-back [aria-hidden] {
font-size: 18px;
line-height: 1;
color: rgba(255, 255, 255, 0.72);
}
.menu-group {
display: grid;
gap: 2px;
}
.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;
}
@@ -0,0 +1,116 @@
<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
>
<track kind="captions" src="/docs/demos/captions-button/captions.vtt" srclang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srclang="es" label="Spanish" />
</hlsjs-video>
<div class="menu-bar">
<button type="button" commandfor="settings-menu" class="settings-trigger">Settings</button>
<media-menu id="settings-menu" side="top" align="end" class="menu">
<media-menu-view class="menu-panel">
<media-menu-item commandfor="quality-menu" type="quality" class="menu-item">
<span>Quality</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="audio-menu" type="audio-track" class="menu-item">
<span>Audio</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="speed-menu" type="playback-rate" class="menu-item">
<span>Speed</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item commandfor="captions-menu" type="captions" class="menu-item">
<span>Captions</span>
<span class="menu-value">
<media-menu-item-value></media-menu-item-value>
<span aria-hidden="true"></span>
</span>
</media-menu-item>
<media-menu-item class="menu-item">Copy link</media-menu-item>
</media-menu-view>
<media-menu id="quality-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Quality
</media-menu-back>
<media-quality-radio-group class="menu-group">
<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>
<media-menu id="audio-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Audio
</media-menu-back>
<media-audio-track-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-audio-track-radio-group>
</media-menu>
<media-menu id="speed-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Speed
</media-menu-back>
<media-playback-rate-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-playback-rate-radio-group>
</media-menu>
<media-menu id="captions-menu" class="menu-panel">
<media-menu-back class="menu-back">
<span aria-hidden="true"></span>
Captions
</media-menu-back>
<media-captions-radio-group class="menu-group">
<template>
<media-menu-radio-item class="menu-item">
<span data-part="label"></span>
<media-menu-item-indicator force-mount class="menu-indicator"></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-captions-radio-group>
</media-menu>
</media-menu>
</div>
</media-container>
</video-player>
@@ -0,0 +1,7 @@
import '@videojs/html/video/player';
import '@videojs/html/media/hlsjs-video';
import '@videojs/html/ui/menu';
import '@videojs/html/ui/quality-radio-group';
import '@videojs/html/ui/audio-track-radio-group';
import '@videojs/html/ui/playback-rate-radio-group';
import '@videojs/html/ui/captions-radio-group';
@@ -0,0 +1,162 @@
.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;
--menu-transition-duration: 220ms;
position: relative;
box-sizing: border-box;
width: var(--media-menu-width);
min-width: 180px;
max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
height: var(--media-menu-height);
max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
padding: 6px;
margin: 0;
overflow: hidden;
overscroll-behavior: none;
font-size: 14px;
color: white;
background: rgba(0, 0, 0, 0.88);
border: 0;
border-radius: 8px;
backdrop-filter: blur(10px);
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: width, height, opacity, filter;
}
.menu-panel {
position: absolute;
inset: 0;
display: grid;
gap: 2px;
padding: 6px;
overflow: auto;
overscroll-behavior: none;
outline: none;
translate: 0 0;
transition-timing-function: ease-in-out;
transition-duration: var(--menu-transition-duration);
transition-property: translate, filter;
will-change: translate;
}
.menu-panel[data-starting-style],
.menu-panel[data-ending-style] {
overflow: hidden;
}
.menu-panel[data-menu-root-view][data-menu-view-state="inactive"] {
filter: blur(8px);
translate: -100% 0;
}
.menu-panel[data-submenu] {
z-index: 1;
}
.menu-panel[data-submenu]:not([data-open], [data-ending-style]) {
translate: -100% 0;
transition-property: none;
}
.menu-panel[data-submenu][data-starting-style],
.menu-panel[data-submenu][data-ending-style] {
pointer-events: none;
filter: blur(8px);
}
.menu-panel[data-submenu][data-starting-style][data-direction="forward"],
.menu-panel[data-submenu][data-ending-style][data-direction="back"] {
translate: 100% 0;
}
.menu-panel[data-submenu][data-ending-style][data-direction="forward"],
.menu-panel[data-submenu][data-starting-style][data-direction="back"] {
translate: -100% 0;
}
.menu-item,
.menu-back {
display: flex;
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],
.menu-back:hover {
background: rgba(255, 255, 255, 0.16);
}
.menu-back {
gap: 8px;
justify-content: flex-start;
}
.menu-value {
display: inline-flex;
gap: 8px;
align-items: center;
color: rgba(255, 255, 255, 0.72);
}
.menu-group {
display: grid;
gap: 2px;
}
.menu-tier {
margin-left: 2px;
font-size: 10px;
}
.menu-badge {
margin-left: auto;
color: rgba(255, 255, 255, 0.72);
}
.menu-back [aria-hidden],
[role="menuitemradio"] [aria-hidden],
.menu-item [aria-hidden] {
font-size: 18px;
line-height: 1;
color: rgba(255, 255, 255, 0.72);
}
.menu-indicator {
opacity: 0;
}
[role="menuitemradio"][aria-checked="true"] .menu-indicator {
opacity: 1;
}
@@ -0,0 +1,254 @@
import {
createPlayer,
Menu,
useAudioTrackOptions,
useCaptionsOptions,
usePlaybackRateOptions,
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 SettingsMenu(): ReactNode {
const playbackRate = usePlaybackRateOptions();
const quality = useQualityOptions();
const audioTrack = useAudioTrackOptions();
const captions = useCaptionsOptions();
const hasPlaybackRate = playbackRate?.state.availability === 'available';
const hasQuality = quality?.state.availability === 'available';
const hasAudioTrack = audioTrack?.state.availability === 'available';
const hasCaptions = captions?.state.availability === 'available';
if (!hasPlaybackRate && !hasQuality && !hasAudioTrack && !hasCaptions) return null;
return (
<Menu.Root side="top" align="end">
<Menu.Trigger className="settings-trigger" aria-label="Settings" render={<button type="button" />}>
Settings
</Menu.Trigger>
<Menu.Content className="menu">
<Menu.View className="menu-panel">
{hasQuality ? (
<Menu.Root>
<Menu.Trigger
type="quality"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Quality</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Quality
</Menu.Back>
<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>
) : null}
{hasAudioTrack ? (
<Menu.Root>
<Menu.Trigger
type="audio-track"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Audio</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Audio
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={audioTrack.value}
onValueChange={audioTrack.setValue}
aria-label="Audio tracks"
>
{audioTrack.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === audioTrack.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
{hasPlaybackRate ? (
<Menu.Root>
<Menu.Trigger
type="playback-rate"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Speed</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Speed
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={playbackRate.value}
onValueChange={playbackRate.setValue}
aria-label="Playback rate"
>
{playbackRate.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === playbackRate.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
{hasCaptions ? (
<Menu.Root>
<Menu.Trigger
type="captions"
className="menu-item"
render={(props) => (
<div {...props}>
<span>Captions</span>
<span className="menu-value">
<Menu.ItemValue />
<span aria-hidden="true"></span>
</span>
</div>
)}
/>
<Menu.Content className="menu-panel">
<Menu.Back className="menu-back">
<span aria-hidden="true"></span>
Captions
</Menu.Back>
<Menu.RadioGroup
className="menu-group"
value={captions.value}
onValueChange={captions.setValue}
aria-label="Captions"
>
{captions.options.map((option) => (
<Menu.RadioItem
key={option.value}
value={option.value}
disabled={option.disabled}
className="menu-item"
>
<span>{option.label}</span>
<Menu.ItemIndicator
checked={option.value === captions.value}
forceMount
className="menu-indicator"
>
</Menu.ItemIndicator>
</Menu.RadioItem>
))}
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
) : null}
<Menu.Item className="menu-item" onSelect={() => navigator.clipboard?.writeText(window.location.href)}>
Copy link
</Menu.Item>
</Menu.View>
</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>
<track kind="captions" src="/docs/demos/captions-button/captions.vtt" srcLang="en" label="English" />
<track kind="subtitles" src="/docs/demos/captions-button/captions.vtt" srcLang="es" label="Spanish" />
</HlsJsVideo>
<div className="menu-bar">
<SettingsMenu />
</div>
</Player.Container>
</Player.Provider>
);
}
@@ -0,0 +1,79 @@
---
title: AudioTrackRadioGroup
frameworkTitle:
html: media-audio-track-radio-group
description: A menu radio group for selecting an audio track
---
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Creates radio items from the player audio track state and selects the enabled track.
## Anatomy
<FrameworkCase frameworks={["react"]}>
```tsx
const audioTrack = useAudioTrackOptions();
<Menu.RadioGroup value={audioTrack.value} onValueChange={audioTrack.setValue}>
<Menu.GroupLabel>Audio</Menu.GroupLabel>
{audioTrack.options.map((option) => (
<Menu.RadioItem key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
<Menu.ItemIndicator checked={option.value === audioTrack.value} />
</Menu.RadioItem>
))}
</Menu.RadioGroup>
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<media-audio-track-radio-group>
<template>
<media-menu-radio-item>
<span data-part="label"></span>
<media-menu-item-indicator></media-menu-item-indicator>
</media-menu-radio-item>
</template>
</media-audio-track-radio-group>
```
</FrameworkCase>
## Behavior
The group is available when the configured media exposes more than one audio track. Labels use the track label, then language, then kind. Pass `formatTrack` to customize the visible labels.
<FrameworkCase frameworks={["react"]}>
`useAudioTrackOptions` returns `null` when the <DocsLink slug="reference/feature-audio-track">audio track feature</DocsLink> is not configured. Use the returned options with <DocsLink slug="reference/menu">`Menu.RadioGroup`</DocsLink>.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
`<media-audio-track-radio-group>` generates `<media-menu-radio-item>` children. Add an optional `<template>` with a single `<media-menu-radio-item>` to customize each generated item.
</FrameworkCase>
## Styling
| Attribute | Values | Description |
| --- | --- | --- |
| `data-audio-track` | `string` | Current audio track value. |
| `data-disabled` | Present / absent | Present when audio track selection is disabled. |
| `data-availability` | `"available"` / `"unavailable"` | Whether multiple audio tracks are available. |
<FrameworkCase frameworks={["html"]}>
```css
media-audio-track-radio-group[data-availability="unavailable"] {
display: none;
}
```
</FrameworkCase>
## Accessibility
The group uses the menu radio group pattern. It receives an accessible label from the `label` prop or defaults to `Audio`.
<FrameworkCase frameworks={["html"]}>
<ComponentReference component="AudioTrackRadioGroup" />
</FrameworkCase>
@@ -41,6 +41,8 @@ Toggles captions and subtitles on and off. The button checks the media's text tr
When no caption or subtitle tracks are present, `data-availability="unavailable"` is set. Use this to hide the button when there are no tracks to toggle.
When `menuTrigger` is enabled and multiple caption or subtitle tracks are available, activation opens the linked captions menu instead of toggling captions directly. React enables `menuTrigger` automatically when a `CaptionsButton` is rendered inside `Menu.Trigger`.
## Styling
Style the button based on active state:
@@ -62,6 +64,8 @@ media-captions-button[data-availability="unavailable"] {
Renders a `<button>` with an automatic `aria-label`: "Disable captions" when active, "Enable captions" when inactive. Override with the `label` prop. Keyboard activation: <kbd>Enter</kbd> / <kbd>Space</kbd>.
In menu-trigger mode, the button behaves as a menu trigger and reflects menu state through the trigger attributes.
## Examples
### Basic Usage
+192
View File
@@ -0,0 +1,192 @@
---
title: Menu
frameworkTitle:
html: media-menu
description: A composable menu component for settings, option selection, and actions
---
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
import StyleCase from "@/components/docs/StyleCase.astro";
import Demo from "@/components/docs/demos/Demo.astro";
{/* React demos */}
import BasicUsageDemoReact from "@/components/docs/demos/menu/react/css/BasicUsage";
import basicUsageReactTsx from "@/components/docs/demos/menu/react/css/BasicUsage.tsx?raw";
import basicUsageReactCss from "@/components/docs/demos/menu/react/css/BasicUsage.css?raw";
{/* HTML demos */}
import BasicUsageDemoHtml from "@/components/docs/demos/menu/html/css/BasicUsage.astro";
import basicUsageHtml from "@/components/docs/demos/menu/html/css/BasicUsage.html?raw";
import basicUsageHtmlCss from "@/components/docs/demos/menu/html/css/BasicUsage.css?raw";
import basicUsageHtmlTs from "@/components/docs/demos/menu/html/css/BasicUsage.ts?raw";
## Anatomy
A submenu is a menu inside another menu. The submenu trigger lives in the parent list, and the submenu content is the next view.
<FrameworkCase frameworks={["react"]}>
```tsx
<Menu.Root>
<Menu.Trigger />
<Menu.Content>
<Menu.View>
{/* Parent menu item that opens a submenu */}
<Menu.Root>
{/* Submenu trigger */}
<Menu.Trigger>
<Menu.ItemValue />
</Menu.Trigger>
{/* Submenu content */}
<Menu.Content>
<Menu.Back />
<Menu.RadioGroup>
<Menu.GroupLabel />
<Menu.RadioItem>
<Menu.ItemIndicator />
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Content>
</Menu.Root>
{/* Other parent menu items */}
<Menu.Group>
<Menu.GroupLabel />
<Menu.CheckboxItem>
<Menu.ItemIndicator />
</Menu.CheckboxItem>
</Menu.Group>
<Menu.Separator />
<Menu.Item />
</Menu.View>
</Menu.Content>
</Menu.Root>
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<button type="button" commandfor="settings-menu">Settings</button>
<media-menu id="settings-menu">
<media-menu-view>
<!-- Parent menu item that opens a submenu -->
<media-menu-item commandfor="submenu">
<media-menu-item-value></media-menu-item-value>
</media-menu-item>
<!-- Other parent menu items -->
<media-menu-group>
<media-menu-group-label></media-menu-group-label>
<media-menu-checkbox-item>
<media-menu-item-indicator></media-menu-item-indicator>
</media-menu-checkbox-item>
</media-menu-group>
<media-menu-separator></media-menu-separator>
<media-menu-item></media-menu-item>
</media-menu-view>
<!-- Submenu content -->
<media-menu id="submenu">
<media-menu-back></media-menu-back>
<media-menu-radio-group>
<media-menu-group-label></media-menu-group-label>
<media-menu-radio-item>
<media-menu-item-indicator></media-menu-item-indicator>
</media-menu-radio-item>
</media-menu-radio-group>
</media-menu>
</media-menu>
```
</FrameworkCase>
## Behavior
Menus open from a trigger and close when you select an item, click outside, move focus away, or press <kbd>Escape</kbd>. Root menus are positioned against their trigger with `side` and `align`.
Create a submenu by putting another menu inside the root menu's content. Its trigger stays in the root list, and its content becomes the next view. In HTML, give the submenu a matching `id` and point the trigger at it with `commandfor`. Wrap the root list in `Menu.View` or `<media-menu-view>` when the root list and submenus share one animated viewport.
Set `type="playback-rate"`, `type="quality"`, `type="audio-track"`, or `type="captions"` on a submenu trigger when it represents a media setting. `Menu.ItemValue` and `<media-menu-item-value>` read that setting context and display the current value, such as `1.5x`, `Auto`, `English`, or `Off`. See <DocsLink slug="reference/quality-radio-group">QualityRadioGroup</DocsLink> and <DocsLink slug="reference/audio-track-radio-group">AudioTrackRadioGroup</DocsLink> for the generated option groups.
## Styling
Use data attributes to style open state, highlighted items, selected radio items, and submenu views:
`data-highlighted` is added to the current menu item. The highlighted item is the item that keyboard selection will act on. It changes when the menu opens, when the pointer moves over another item, when the user presses arrow keys, or when type-ahead search finds a match.
<FrameworkCase frameworks={["react"]}>
```css
.menu[data-open] {
opacity: 1;
}
.menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
[role="menuitemradio"][aria-checked="true"] {
font-weight: 600;
}
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```css
media-menu[data-open] {
opacity: 1;
}
media-menu-item[data-highlighted] {
background: rgba(255, 255, 255, 0.16);
}
media-menu-radio-item[aria-checked="true"] {
font-weight: 600;
}
```
</FrameworkCase>
## Accessibility
Menu content renders with `role="menu"`. Items use `menuitem`, `menuitemradio`, or `menuitemcheckbox` roles. Radio and checkbox items reflect selection with `aria-checked`.
Keyboard controls:
- <kbd>Enter</kbd> / <kbd>Space</kbd>: Select the highlighted item.
- <kbd>Arrow Up</kbd> / <kbd>Arrow Down</kbd>: Move between items.
- <kbd>Arrow Right</kbd>: Open a submenu.
- <kbd>Arrow Left</kbd>: Return to the parent menu.
- <kbd>Escape</kbd>: Close the root menu or return from a submenu.
Use `Menu.GroupLabel` or `<media-menu-group-label>` inside grouped choices so the group receives an accessible label.
## Examples
### Basic usage
<FrameworkCase frameworks={["react"]}>
<StyleCase styles={["css"]}>
<Demo files={[
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
]}>
<BasicUsageDemoReact client:idle />
</Demo>
</StyleCase>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
<StyleCase styles={["css"]}>
<Demo files={[
{ title: "index.html", code: basicUsageHtml, lang: "html" },
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
]}>
<BasicUsageDemoHtml />
</Demo>
</StyleCase>
</FrameworkCase>
<ComponentReference component="Menu" />
@@ -39,6 +39,8 @@ import basicUsageHtmlTs from "@/components/docs/demos/playback-rate-button/html/
Cycles through playback rates on click. The default rate list is `[0.2, 0.5, 0.7, 1, 1.2, 1.5, 1.7, 2]`. After the last rate, it wraps back to the first. If the current rate isn't in the list (e.g., set programmatically), the button jumps to the next rate greater than the current one.
When `menuTrigger` is enabled, activation opens the linked playback-rate menu instead of cycling through rates. Use this mode when the button is the trigger for a `Menu` that contains playback-rate options.
## Styling
Display the current rate using the `data-rate` attribute:
@@ -65,6 +67,8 @@ Use the `render` prop for rate display:
Renders a `<button>` with an automatic `aria-label` of `"Playback rate {rate}"` (e.g., `"Playback rate 1.5"`). Override with the `label` prop. Keyboard activation: <kbd>Enter</kbd> / <kbd>Space</kbd>.
In menu-trigger mode, the button behaves as a menu trigger and reflects menu state through the trigger attributes.
## Examples
### Basic Usage
@@ -0,0 +1,81 @@
---
title: QualityRadioGroup
frameworkTitle:
html: media-quality-radio-group
description: A menu radio group for selecting video quality
---
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
Creates radio items from the player video rendition state and selects automatic or manual quality.
## Anatomy
<FrameworkCase frameworks={["react"]}>
```tsx
const quality = useQualityOptions();
<Menu.RadioGroup value={quality.value} onValueChange={quality.setValue}>
<Menu.GroupLabel>Quality</Menu.GroupLabel>
{quality.options.map((option) => (
<Menu.RadioItem key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
<Menu.ItemIndicator checked={option.value === quality.value} />
</Menu.RadioItem>
))}
</Menu.RadioGroup>
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<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>
```
</FrameworkCase>
## 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.
<FrameworkCase frameworks={["react"]}>
`useQualityOptions` returns `null` when the <DocsLink slug="reference/feature-quality">quality feature</DocsLink> is not configured. Use the returned options with <DocsLink slug="reference/menu">`Menu.RadioGroup`</DocsLink>.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
`<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.
</FrameworkCase>
## 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. |
<FrameworkCase frameworks={["html"]}>
```css
media-quality-radio-group[data-availability="unavailable"] {
display: none;
}
```
</FrameworkCase>
## Accessibility
The group uses the menu radio group pattern. It receives an accessible label from the `label` prop or defaults to `Quality`.
<FrameworkCase frameworks={["html"]}>
<ComponentReference component="QualityRadioGroup" />
</FrameworkCase>
+3
View File
@@ -62,17 +62,20 @@ export const sidebar: Sidebar = [
{ slug: 'reference/player-container' },
// sorted alphabetically
{ slug: 'reference/airplay-button' },
{ slug: 'reference/audio-track-radio-group' },
{ slug: 'reference/buffering-indicator' },
{ slug: 'reference/captions-button' },
{ slug: 'reference/cast-button' },
{ slug: 'reference/controls' },
{ slug: 'reference/fullscreen-button' },
{ slug: 'reference/menu' },
{ slug: 'reference/mute-button' },
{ slug: 'reference/pip-button' },
{ slug: 'reference/play-button' },
{ slug: 'reference/playback-rate-button' },
{ slug: 'reference/popover' },
{ slug: 'reference/poster' },
{ slug: 'reference/quality-radio-group' },
{ slug: 'reference/seek-button' },
{ slug: 'reference/slider' },
{ slug: 'reference/thumbnail' },