Skip to content

ReferenceMenus

Menu

A composable menu component for settings, option selection, and actions

Import

import { Menu } from '@videojs/react';

Anatomy

A root menu owns one positioned Popup. Nested Content elements share that Popup with the root Content.

<Menu.Root>
  <Menu.Trigger />
  <Menu.Popup>
    <Menu.Content>
      {/* Parent menu item that opens a submenu */}
      <Menu.Root>
        <Menu.Trigger />

        <Menu.Content>
          <Menu.Item>Back</Menu.Item>
          <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.Content>
  </Menu.Popup>
</Menu.Root>

Behavior

Menus open from a trigger and close when you select an item, click outside, move focus away, or press Escape. Root menus use side and align as their preferred placement. When the preferred side overflows the positioning boundary, the menu uses the opposite side if it has more space.

Root menus inside a player container join its popup group. Opening one closes any other root menu or popover that is open in the same container. Submenus remain part of their root menu instead of registering separately.

Create a submenu by nesting another Menu.Root in the parent Menu.Content; its Menu.Trigger behaves as a parent item and its Menu.Content becomes the active Content.

Media option groups share their option state with the enclosing menu: the selected value, and whether the options are disabled, hidden, or available. The menu’s trigger inherits the disabled and hidden state and exposes data-availability, and the menu closes if its options disappear while open. See PlaybackRateRadioGroup, QualityRadioGroup, AudioTrackRadioGroup, and CaptionsRadioGroup.

Wrap a submenu’s Menu.Trigger and Menu.Content in the group’s Root part, and render its Value part inside the trigger to show the current selection:

<Menu.Root>
  <QualityRadioGroup.Root>
    <Menu.Trigger>
      Quality
      <QualityRadioGroup.Value />
    </Menu.Trigger>
    <Menu.Content>
      <QualityRadioGroup.Options renderItem={...} />
    </Menu.Content>
  </QualityRadioGroup.Root>
</Menu.Root>

Styling

Use data attributes to style open state, highlighted items, selected radio items, and submenu views:

data-highlighted identifies the current menu item. Its value is "pointer" when pointer movement caused the highlight and an empty string for other highlights, including keyboard navigation and type-ahead search. Use [data-highlighted] to match any highlighted item or [data-highlighted=""] to match highlights that were not caused by pointer movement.

The root Menu.Popup receives data-open, data-side, and data-align. Each Menu.Content receives menu state such as data-open and data-submenu; a Content with an open logical child receives data-child-open.

Every navigable item receives data-item. Use [data-item] when one rule should target regular, radio, checkbox, and submenu-trigger items together.

.menu-popup[data-open] {
  opacity: 1;
}

.menu-content[data-child-open] {
  translate: -100% 0;
}

.menu-item[data-highlighted] {
  background: rgba(255, 255, 255, 0.16);
  transition: background-color 100ms ease-in-out;
}

.menu-item[data-highlighted=""] {
  transition-duration: 0ms;
}

[role="menuitemradio"][aria-checked="true"] {
  font-weight: 600;
}

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:

  • Enter / Space: Select the highlighted item.
  • Arrow Up / Arrow Down: Move between items.
  • Arrow Right: Open a submenu.
  • Arrow Left: Return to the parent menu.
  • Escape: Close the root menu or return from a submenu.

Use Menu.GroupLabel inside grouped choices so the group receives an accessible label.

Examples

Basic usage

import {
  Container,
  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/s41JYeqIpBMBzE4OzxDyGR2yrp2hD1CQ6gJN9SlVGDQ.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.Popup className="menu">
        <Menu.Content className="menu-content">
          {hasQuality ? (
            <Menu.Root>
              <Menu.Trigger
                className="menu-item"
                render={(props) => (
                  <div {...props}>
                    <span>Quality</span>
                    <span className="menu-value">
                      {quality.selectedLabel}
                      <span aria-hidden="true">›</span>
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="menu-panel">
                <Menu.Item className="menu-back">
                  <span aria-hidden="true">‹</span>
                  Quality
                </Menu.Item>
                <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
                className="menu-item"
                render={(props) => (
                  <div {...props}>
                    <span>Audio</span>
                    <span className="menu-value">
                      {audioTrack.selectedLabel}
                      <span aria-hidden="true">›</span>
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="menu-panel">
                <Menu.Item className="menu-back">
                  <span aria-hidden="true">‹</span>
                  Audio
                </Menu.Item>
                <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
                className="menu-item"
                render={(props) => (
                  <div {...props}>
                    <span>Speed</span>
                    <span className="menu-value">
                      {playbackRate.selectedLabel}
                      <span aria-hidden="true">›</span>
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="menu-panel">
                <Menu.Item className="menu-back">
                  <span aria-hidden="true">‹</span>
                  Speed
                </Menu.Item>
                <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
                className="menu-item"
                render={(props) => (
                  <div {...props}>
                    <span>Captions</span>
                    <span className="menu-value">
                      {captions.selectedLabel}
                      <span aria-hidden="true">›</span>
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="menu-panel">
                <Menu.Item className="menu-back">
                  <span aria-hidden="true">‹</span>
                  Captions
                </Menu.Item>
                <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.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

export default function BasicUsage() {
  return (
    <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>
      </Container>
    </Player>
  );
}

API Reference

Root

Props

PropTypeDefaultDetails
align'start' | 'center' | 'end''start'
boundary'viewport' | 'container' | string & {}—
closeOnEscapebooleantrue
closeOnOutsideClickbooleantrue
defaultOpenbooleanfalse
openbooleanfalse
side'top' | 'bottom' | 'left' | 'right''bottom'

State

State is accessible via the render, className, and style props.

PropertyTypeDetails
transitionStartingboolean
transitionEndingboolean
openboolean
status'idle' | 'starting' | 'ending'
side'top' | 'bottom' | 'left' | 'right' |...
align'start' | 'center' | 'end' | undefined
isSubmenuboolean

CSS custom properties

VariableDescription
--media-menu-widthWidth of the active menu panel (px).
--media-menu-heightHeight of the active menu panel (px).
--media-menu-available-widthViewport-constrained max width for the menu (px).
--media-menu-available-heightViewport-constrained max height for the menu (px).

CheckboxItem

A checkbox-style menu item. Renders a <div> with role="menuitemcheckbox".

Props

PropTypeDefaultDetails
checkedboolean—
classNamestring | function—
disabledboolean—
onCheckedChangefunction—
renderReactElement | function—
styleCSSProperties | function—

Data attributes

AttributeDescription
data-itemPresent on all navigable item types: Item, RadioItem, CheckboxItem, and the Trigger when acting as a submenu trigger inside a parent menu. Use [data-item] as a shared selector to target all item types at once.
data-highlightedPresent when the item is highlighted. Set to pointer when pointer movement caused the highlight; otherwise empty.

Content

Accessible item and focus scope for exactly one root or nested menu page.

Props

PropTypeDefaultDetails
classNamestring | function—
renderReactElement | function—
styleCSSProperties | function—

Group

Groups related menu items. Renders a <div> with role="group".

Props

PropTypeDefaultDetails
classNamestring | function—
renderReactElement | function—
styleCSSProperties | function—

GroupLabel

Non-interactive label for a group of items. Renders a <div>.

Props

PropTypeDefaultDetails
classNamestring | function—
renderReactElement | function—
styleCSSProperties | function—

Item

A single action in the menu. Renders a <div> with role="menuitem".

Props

PropTypeDefaultDetails
classNamestring | function—
disabledboolean—
onSelectfunction—
renderReactElement | function—
styleCSSProperties | function—

Data attributes

AttributeDescription
data-itemPresent on all navigable item types: Item, RadioItem, CheckboxItem, and the Trigger when acting as a submenu trigger inside a parent menu. Use [data-item] as a shared selector to target all item types at once.
data-highlightedPresent when the item is highlighted. Set to pointer when pointer movement caused the highlight; otherwise empty.

ItemIndicator

Visual indicator for a checked state. Only renders when checked is true (or forceMount is set).

Props

PropTypeDefaultDetails
checkedboolean—
classNamestring | function—
forceMountboolean—
renderReactElement | function—
styleCSSProperties | function—

Positioned menu surface that contains one or more sibling Content pages.

PropTypeDefaultDetails
classNamestring | function—
keepMountedboolean—
renderReactElement | function—
styleCSSProperties | function—

RadioGroup

A group of mutually exclusive radio items. Renders a <div> with role="group".

Props

PropTypeDefaultDetails
classNamestring | function—
onValueChangefunction—
renderReactElement | function—
styleCSSProperties | function—
valuestring—

RadioItem

A radio-style menu item. Renders a <div> with role="menuitemradio".

Props

PropTypeDefaultDetails
classNamestring | function—
disabledboolean—
renderReactElement | function—
styleCSSProperties | function—
valuestring—

Data attributes

AttributeDescription
data-itemPresent on all navigable item types: Item, RadioItem, CheckboxItem, and the Trigger when acting as a submenu trigger inside a parent menu. Use [data-item] as a shared selector to target all item types at once.
data-highlightedPresent when the item is highlighted. Set to pointer when pointer movement caused the highlight; otherwise empty.

Separator

Visual divider between groups of items. Renders a <div> with role="separator".

Props

PropTypeDefaultDetails
classNamestring | function—
renderReactElement | function—
styleCSSProperties | function—

Trigger

Button that toggles the menu visibility. At root level renders a <button>. When inside a parent menu (as a submenu trigger), renders as a <div role="menuitem"> that opens the submenu on click or ArrowRight.

Props

PropTypeDefaultDetails
classNamestring | function—
disabledboolean—
onClickMouseEventHandler<HTMLButtonElement |...—
onKeyDownKeyboardEventHandler<HTMLButtonElemen...—
renderReactElement | function—
styleCSSProperties | function—

Data attributes

AttributeDescription
data-itemPresent on all navigable item types: Item, RadioItem, CheckboxItem, and the Trigger when acting as a submenu trigger inside a parent menu. Use [data-item] as a shared selector to target all item types at once.
data-highlightedPresent when the item is highlighted. Set to pointer when pointer movement caused the highlight; otherwise empty.