Skip to content
FrameworkStyle

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 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.

In React, 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. In HTML, add another <media-menu-content> as a sibling, give it an id, and point the parent <media-menu-item> at it with commandfor.

Media option groups can render the current selection beside a submenu trigger. See PlaybackRateRadioGroup, QualityRadioGroup, AudioTrackRadioGroup, and CaptionsRadioGroup.

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 or <media-menu> receives data-open, data-side, and data-align. Each 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 or <media-menu-group-label> 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

State

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

PropertyTypeDetails

CSS custom properties

VariableDetails

CheckboxItem

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

Props

PropTypeDefaultDetails

Data attributes

AttributeTypeDetails

Content

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

Props

PropTypeDefaultDetails

Group

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

Props

PropTypeDefaultDetails

GroupLabel

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

Props

PropTypeDefaultDetails

Item

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

Props

PropTypeDefaultDetails

Data attributes

AttributeTypeDetails

ItemIndicator

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

Props

PropTypeDefaultDetails

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

PropTypeDefaultDetails

RadioGroup

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

Props

PropTypeDefaultDetails

RadioItem

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

Props

PropTypeDefaultDetails

Data attributes

AttributeTypeDetails

Separator

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

Props

PropTypeDefaultDetails

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

Data attributes

AttributeTypeDetails