Skip to content
FrameworkStyle

Customize skins

Learn the public customization surface for packaged Video.js skins and when to eject

Video.js v10 comes with two pre-built skins: Default and Minimal. Start with their public CSS custom properties. When you need to change controls, layout, or interactions, eject the closest skin by copying its code into your project and making it your own.

Basic customization

Set these CSS custom properties directly on the skin:

<VideoSkin
  style={
    {
      '--media-accent-color': '#f5c518',
      '--media-border-radius': '1rem',
    } as React.CSSProperties
  }
>
  <Video src="video.mp4" />
</VideoSkin>
Property Name Description Type Example
--media-accent-color The color of slider fills and accented controls <color> #f5c518
--media-accent-text-color The color of text and icons shown on the accent color <color> black
--media-border-radius The border radius of the media player A valid border-radius value 1rem
--media-scale-unit The base sizing unit for skin spacing and icons <length> 1rem

If you omit --media-accent-text-color, the skins choose a contrasting text color from --media-accent-color using contrast-color(). You only need to set it when you want to override that result.

Set the audio color scheme

The audio skins adapt their main palette for light and dark mode using light-dark(). It uses the inherited color-scheme, so set color-scheme on the player or an ancestor:

:root {
  color-scheme: light dark;
}

The video skins keep their dark control presentation in both color schemes.

Scale with the document font size

Vanilla CSS and Tailwind skins use --media-scale-unit as the base for control spacing and icon sizes. Vanilla CSS skins also use it for font sizes. It defaults to 16px, so player sizing does not change with the document root font size or the app’s Tailwind spacing scale. Set it to 1rem on the player or an ancestor to make the UI scale with the document root font size:

.player {
  --media-scale-unit: 1rem;
}

You can also add your own class names to the skins.

Where packaged customization stops

The four properties above are the public styling controls for packaged skins. Don’t depend on the elements, classnames, or other custom properties inside a packaged skin. They may change between releases.

Packaged video skins also let you replace the poster through the renderPoster prop.

See Poster.

For anything deeper, eject instead of reaching into the packaged skin. There are no packaged-skin options for adding, removing, or rearranging controls, or replacing its built-in feedback and interactions.

Eject a skin

Eject as soon as the packaged skin no longer matches the player you need. Once ejected, removing a control is removing a component, and changing an overlay or interaction is editing code you own.

An ejected skin is app code, so its markup, class names, and styles are yours to change. If none of the packaged skins is a useful starting point, build a custom UI from the player, container, and individual UI components instead.

Soon, we’ll have a CLI that ejects skins in your preferred framework and style. For now, we invite you to try these copy-paste-ready implementations.

To eject a skin:

  1. Choose the closest implementation below.
  2. Copy every file shown for your framework into your project.
  3. Replace the packaged skin with the copied component or player markup, then edit the files you own.

Default video skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { audioText, captionsText, playbackRateText, qualityText, settingsText, speedText } from '@videojs/core/i18n/text/menu';
import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, GearIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, QualityIcon, RestartIcon, SpeechIcon, SpeedIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
import { Poster, useTranslator, Container, usePlayer, AirPlayButton, useAudioTrackOptions, AudioTrackRadioGroup, BufferingIndicator, CaptionsButton, CaptionsRadioGroup, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, usePlaybackRateOptions, PlaybackRateRadioGroup, Popover, useQualityOptions, QualityRadioGroup, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './player.css';
import { Player } from './player';

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;

function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
  return <ChevronIcon className={`media-icon media-menu__chevron ${flipped ? 'media-icon--flipped' : undefined}`} />;
}

function SettingsMenu(): ReactNode {
  const t = useTranslator();
  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="center">
      <Tooltip.Root side="top">
        <Tooltip.Trigger
          render={
            <Menu.Trigger aria-label={t(settingsText)} className="media-button--settings" render={<Button />}>
              <GearIcon className="media-icon media-icon--settings" />
            </Menu.Trigger>
          }
        />
        <Tooltip.Popup className="media-surface media-tooltip">
          <Tooltip.Label>{t(settingsText)}</Tooltip.Label>
        </Tooltip.Popup>
      </Tooltip.Root>
      <Menu.Popup className="media-surface media-popover media-menu media-menu--settings">
        <Menu.Content className="media-menu__content">
          {hasQuality ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <QualityIcon className="media-icon" />
                    <span>{t(qualityText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {quality.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(qualityText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <QualityRadioGroup
                  className="media-menu__group"
                  aria-label={t(qualityText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">
                        {item.label}
                        {item.tier ? <sup className="media-menu__tier">{item.tier}</sup> : null}
                      </bdi>
                      {item.badge ? <span className="media-badge">{item.badge}</span> : null}
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasAudioTrack ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <SpeechIcon className="media-icon" />
                    <span>{t(audioText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {audioTrack.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(audioText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <AudioTrackRadioGroup
                  className="media-menu__group"
                  aria-label={t(audioText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasPlaybackRate ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <SpeedIcon className="media-icon" />
                    <span>{t(speedText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {playbackRate.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(speedText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <PlaybackRateRadioGroup
                  className="media-menu__group"
                  aria-label={t(playbackRateText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasCaptions ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <CaptionsOffIcon className="media-icon" />
                    <span>{t(captionsText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {captions.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(captionsText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <CaptionsRadioGroup
                  className="media-menu__group"
                  aria-label={t(captionsText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}
        </Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

function CastControl() {
  return (
    <Tooltip.Root side="top">
      <Tooltip.Trigger
        render={
          <CastButton className="media-button--cast" render={<Button />}>
            <CastEnterIcon className="media-icon media-icon--cast-enter" />
            <CastExitIcon className="media-icon media-icon--cast-exit" />
          </CastButton>
        }
      />
      <Tooltip.Popup className="media-surface media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

function AirPlayControl() {
  return (
    <Tooltip.Root side="top">
      <Tooltip.Trigger
        render={
          <AirPlayButton className="media-button--airplay" render={<Button />}>
            <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
            <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
          </AirPlayButton>
        }
      />
      <Tooltip.Popup className="media-surface media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

function PiPControl() {
  return (
    <Tooltip.Root side="top">
      <Tooltip.Trigger
        render={
          <PiPButton className="media-button--pip" render={<Button />}>
            <PipEnterIcon className="media-icon media-icon--pip-enter" />
            <PipExitIcon className="media-icon media-icon--pip-exit" />
          </PiPButton>
        }
      />
      <Tooltip.Popup className="media-surface media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

function FullscreenControl() {
  return (
    <Tooltip.Root side="top">
      <Tooltip.Trigger
        render={
          <FullscreenButton className="media-button--fullscreen" render={<Button />}>
            <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
            <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
          </FullscreenButton>
        }
      />
      <Tooltip.Popup className="media-surface media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

// ================================================================
// Player
// ================================================================

const SEEK_TIME = 10;

export interface VideoPlayerProps {
  src: string;
  poster?: string | undefined;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

/**
 * @example
 * ```tsx
 * <VideoPlayer
 *   src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
 *   poster="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp"
 * />
 * ```
 */
export function VideoPlayer({ src, poster, className, renderPoster, style, ...rest }: VideoPlayerProps): ReactNode {
  return (
    <Player poster={poster}>
      <Container className={`media-default-skin media-default-skin--video ${className ?? ''}`} style={style} {...rest}>
        <Video src={src} playsInline />

        <Poster render={renderPoster} />

        <BufferingIndicator
          render={(props) => (
            <div {...props} className="media-buffering-indicator">
              <SpinnerIcon className="media-icon" />
            </div>
          )}
        />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog media-surface">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--primary"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <Controls.Root>
          <Controls.Backdrop className="media-overlay" />
          <Controls.Content className="media-surface media-controls media-controls--root">
            <Tooltip.Provider>
              <div className="media-surface media-controls media-controls--primary">
                <div className="media-button-group">
                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <PlayButton className="media-button--play" render={<Button />}>
                          <RestartIcon className="media-icon media-icon--restart" />
                          <PlayIcon className="media-icon media-icon--play" />
                          <PauseIcon className="media-icon media-icon--pause" />
                        </PlayButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <VolumePopover />
                </div>

                <div className="media-time-controls">
                  <Time.Value type="current" className="media-time" />
                  <TimeSlider.Root className="media-slider">
                    <TimeSlider.Chapters
                      className="media-slider__chapters"
                      renderChapter={(props) => (
                        <div {...props} className={`${props.className} media-slider__chapter`}>
                          <TimeSlider.Track className="media-slider__track media-slider__chapter-track">
                            <TimeSlider.Buffer className="media-slider__buffer" />
                            <TimeSlider.Fill className="media-slider__fill" />
                          </TimeSlider.Track>
                        </div>
                      )}
                    />
                    <TimeSlider.Thumb className="media-slider__thumb" />

                    <TimeSlider.Preview overflow="visible" className="media-slider__preview">
                      <div className="media-surface media-thumbnail media-slider__thumbnail">
                        <Slider.Thumbnail className="media-thumbnail__image" />
                        <SpinnerIcon className="media-thumbnail__spinner media-icon" />
                      </div>
                      <div className="media-slider__value">
                        <TimeSlider.ChapterTitle className="media-slider__chapter-title" />
                        <TimeSlider.Value type="pointer" className="media-time" />
                      </div>
                    </TimeSlider.Preview>
                  </TimeSlider.Root>
                  <Time.Value toggle type="remaining" className="media-time" />
                </div>

                <div className="media-button-group">
                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <CaptionsButton className="media-button--captions" render={<Button />}>
                          <CaptionsOffIcon className="media-icon media-icon--captions-off" />
                          <CaptionsOnIcon className="media-icon media-icon--captions-on" />
                        </CaptionsButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <SettingsMenu />
                </div>
              </div>

              <div className="media-surface media-controls media-controls--secondary">
                <div className="media-button-group">
                  <CastControl />
                  <AirPlayControl />
                  <PiPControl />
                  <FullscreenControl />
                </div>
              </div>
            </Tooltip.Provider>
          </Controls.Content>
        </Controls.Root>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="f" action="toggleFullscreen" />
        <Hotkey keys="c" action="toggleSubtitles" />
        <Hotkey keys="i" action="togglePictureInPicture" />
        <Hotkey keys="ArrowRight" action="seekStep" value={SEEK_TIME / 2} />
        <Hotkey keys="ArrowLeft" action="seekStep" value={-(SEEK_TIME / 2)} />
        <Hotkey keys="l" action="seekStep" value={SEEK_TIME} />
        <Hotkey keys="j" action="seekStep" value={-SEEK_TIME} />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
        <Hotkey keys="0-9" action="seekToPercent" />
        <Hotkey keys="Home" action="seekToPercent" value={0} />
        <Hotkey keys="End" action="seekToPercent" value={100} />
        <Hotkey keys=">" action="speedUp" />
        <Hotkey keys="<" action="speedDown" />

        {/* Gestures */}
        <Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
        <Gesture type="tap" action="toggleControls" pointer="touch" />
        <Gesture type="doubletap" action="seekStep" value={-SEEK_TIME} region="left" />
        <Gesture type="doubletap" action="toggleFullscreen" region="center" />
        <Gesture type="doubletap" action="seekStep" value={SEEK_TIME} region="right" />

        {/* Input Indicators */}
        <StatusAnnouncer className="media-sr-only" />
        <div className="media-input-indicator-overlay">
          <VolumeIndicator.Root className="media-surface media-volume-indicator">
            <VolumeIndicator.Fill className="media-volume-indicator__content">
              <VolumeHighIcon className="media-icon media-icon--volume-high" />
              <VolumeLowIcon className="media-icon media-icon--volume-low" />
              <VolumeOffIcon className="media-icon media-icon--volume-off" />
              <VolumeIndicator.Value className="media-volume-indicator__value" />
            </VolumeIndicator.Fill>
          </VolumeIndicator.Root>

          <StatusIndicator.Root
            actions={TOP_STATUS_ACTIONS}
            className="media-surface media-status-indicator media-status-indicator--state"
          >
            <div className="media-status-indicator__content">
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
              <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
              <PipEnterIcon className="media-icon media-icon--pip-enter" />
              <PipExitIcon className="media-icon media-icon--pip-exit" />
              <StatusIndicator.Value className="media-status-indicator__value" />
            </div>
          </StatusIndicator.Root>

          <SeekIndicator.Root className="media-seek-indicator">
            <ChevronIcon className="media-icon media-icon--seek" />
            <SeekIndicator.Value className="media-seek-indicator__value" />
          </SeekIndicator.Root>

          <StatusIndicator.Root
            actions={CENTER_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--playback"
          >
            <PlayIcon className="media-icon media-icon--play" />
            <PauseIcon className="media-icon media-icon--pause" />
          </StatusIndicator.Root>
        </div>
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) return muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="top">
      <Popover.Trigger render={muteButton} />
      <Popover.Popup className="media-surface media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Default audio skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { playbackRateText } from '@videojs/core/i18n/text/menu';
import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
import { useTranslator, Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, usePlaybackRateOptions, PlaybackRateButton, PlaybackRateRadioGroup as PlaybackRateRadioGroupComponent, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster } from '@videojs/react';
import { Audio } from '@videojs/react/audio';
import './player.css';
import { Player } from './player';

function PlaybackRateRadioGroup(): ReactNode {
  const t = useTranslator();

  return (
    <PlaybackRateRadioGroupComponent
      className="media-menu__group"
      aria-label={t(playbackRateText)}
      renderItem={(props, item) => (
        <Menu.RadioItem {...props} className="media-menu__item">
          <span>{item.label}</span>
          <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
            <CheckIcon className="media-icon" />
          </Menu.ItemIndicator>
        </Menu.RadioItem>
      )}
    />
  );
}

function PlaybackRateTrigger(): ReactNode {
  const state = usePlaybackRateOptions();
  if (!state) return null;

  return (
    <Tooltip.Root side="top" boundary="viewport">
      <Tooltip.Trigger
        render={
          <Menu.Trigger
            disabled={state.disabled}
            render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
          />
        }
      />
      <Tooltip.Popup className="media-surface media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

// ================================================================
// Player
// ================================================================

const SEEK_TIME = 10;

export interface AudioPlayerProps {
  src: string;
  style?: CSSProperties;
  className?: string;
}

/**
 * @example
 * ```tsx
 * <AudioPlayer
 *   src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
 * />
 * ```
 */
export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): ReactNode {
  return (
    <Player>
      <Container className={`media-default-skin media-default-skin--audio ${className ?? ''}`} {...rest}>
        <Audio src={src} />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--subtle"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <div className="media-surface media-controls">
          <Tooltip.Provider>
            <div className="media-button-group">
              <span className="media-button--play__wrapper">
                <BufferingIndicator
                  render={(props) => (
                    <div {...props} className="media-buffering-indicator">
                      <SpinnerIcon className="media-icon" />
                    </div>
                  )}
                />
                <Tooltip.Root side="top" boundary="viewport">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-surface media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>
              </span>

              <Tooltip.Root side="top" boundary="viewport">
                <Tooltip.Trigger
                  render={
                    <SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
                      <span className="media-icon__container">
                        <SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
                        <span className="media-icon__label">{SEEK_TIME}</span>
                      </span>
                    </SeekButton>
                  }
                />
                <Tooltip.Popup className="media-surface media-tooltip">
                  <Tooltip.Label />
                  <Tooltip.Shortcut className="media-tooltip__kbd" />
                </Tooltip.Popup>
              </Tooltip.Root>

              <Tooltip.Root side="top" boundary="viewport">
                <Tooltip.Trigger
                  render={
                    <SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
                      <span className="media-icon__container">
                        <SeekIcon className="media-icon media-icon--seek" />
                        <span className="media-icon__label">{SEEK_TIME}</span>
                      </span>
                    </SeekButton>
                  }
                />
                <Tooltip.Popup className="media-surface media-tooltip">
                  <Tooltip.Label />
                  <Tooltip.Shortcut className="media-tooltip__kbd" />
                </Tooltip.Popup>
              </Tooltip.Root>
            </div>

            <div className="media-time-controls">
              <Time.Value type="current" className="media-time" />
              <TimeSlider.Root className="media-slider">
                <TimeSlider.Track className="media-slider__track">
                  <TimeSlider.Buffer className="media-slider__buffer" />
                  <TimeSlider.Fill className="media-slider__fill" />
                </TimeSlider.Track>
                <TimeSlider.Thumb className="media-slider__thumb" />
                <TimeSlider.Preview overflow="visible" className="media-slider__preview">
                  <TimeSlider.Value
                    type="pointer"
                    className="media-surface media-tooltip media-slider__value media-time"
                  />
                </TimeSlider.Preview>
              </TimeSlider.Root>
              <Time.Value toggle type="remaining" className="media-time" />
            </div>

            <div className="media-button-group">
              <Menu.Root side="top" align="center" boundary="viewport">
                <PlaybackRateTrigger />
                <Menu.Popup className="media-surface media-popover media-menu media-menu--playback-rate">
                  <Menu.Content className="media-menu__content">
                    <PlaybackRateRadioGroup />
                  </Menu.Content>
                </Menu.Popup>
              </Menu.Root>

              <VolumePopover />
            </div>
          </Tooltip.Provider>
        </div>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="ArrowRight" action="seekStep" value={5} />
        <Hotkey keys="ArrowLeft" action="seekStep" value={-5} />
        <Hotkey keys="l" action="seekStep" value={10} />
        <Hotkey keys="j" action="seekStep" value={-10} />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
        <Hotkey keys="0-9" action="seekToPercent" />
        <Hotkey keys="Home" action="seekToPercent" value={0} />
        <Hotkey keys="End" action="seekToPercent" value={100} />
        <Hotkey keys=">" action="speedUp" />
        <Hotkey keys="<" action="speedDown" />

        {/* Input Feedback */}
        <StatusAnnouncer className="media-sr-only" />
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) return muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="top" boundary="viewport">
      <Popover.Trigger render={muteButton} />
      <Popover.Popup className="media-surface media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Minimal video skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { audioText, captionsText, playbackRateText, qualityText, settingsText, speedText } from '@videojs/core/i18n/text/menu';
import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, ChevronIcon, FullscreenEnterIcon, FullscreenExitIcon, GearIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, QualityIcon, RestartIcon, SpeechIcon, SpeedIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
import { Poster, useTranslator, Container, usePlayer, AirPlayButton, useAudioTrackOptions, AudioTrackRadioGroup, BufferingIndicator, CaptionsButton, CaptionsRadioGroup, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, Menu, MuteButton, PiPButton, PlayButton, usePlaybackRateOptions, PlaybackRateRadioGroup, Popover, useQualityOptions, QualityRadioGroup, SeekIndicator, Slider, StatusAnnouncer, StatusIndicator, Time, TimeSlider, Tooltip, VolumeIndicator, VolumeSlider } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './player.css';
import { Player } from './player';

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;

function MenuChevron({ flipped = false }: { flipped?: boolean }): ReactNode {
  return <ChevronIcon className={`media-icon media-menu__chevron ${flipped ? 'media-icon--flipped' : undefined}`} />;
}

function SettingsMenu(): ReactNode {
  const t = useTranslator();
  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="center">
      <Tooltip.Root side="top">
        <Tooltip.Trigger
          render={
            <Menu.Trigger aria-label={t(settingsText)} className="media-button--settings" render={<Button />}>
              <GearIcon className="media-icon media-icon--settings" />
            </Menu.Trigger>
          }
        />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label>{t(settingsText)}</Tooltip.Label>
        </Tooltip.Popup>
      </Tooltip.Root>
      <Menu.Popup className="media-popover media-menu media-menu--settings">
        <Menu.Content className="media-menu__content">
          {hasQuality ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <QualityIcon className="media-icon" />
                    <span>{t(qualityText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {quality.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(qualityText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <QualityRadioGroup
                  className="media-menu__group"
                  aria-label={t(qualityText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">
                        {item.label}
                        {item.tier ? <sup className="media-menu__tier">{item.tier}</sup> : null}
                      </bdi>
                      {item.badge ? <span className="media-badge">{item.badge}</span> : null}
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasAudioTrack ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <SpeechIcon className="media-icon" />
                    <span>{t(audioText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {audioTrack.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(audioText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <AudioTrackRadioGroup
                  className="media-menu__group"
                  aria-label={t(audioText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasPlaybackRate ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <SpeedIcon className="media-icon" />
                    <span>{t(speedText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {playbackRate.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(speedText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <PlaybackRateRadioGroup
                  className="media-menu__group"
                  aria-label={t(playbackRateText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}

          {hasCaptions ? (
            <Menu.Root>
              <Menu.Trigger
                className="media-menu__item media-menu__item--submenu"
                render={(props) => (
                  <div {...props}>
                    <CaptionsOffIcon className="media-icon" />
                    <span>{t(captionsText)}</span>
                    <span className="media-menu__hint">
                      <bdi dir="auto" className="media-menu__hint-label">
                        {captions.selectedLabel}
                      </bdi>
                      <MenuChevron />
                    </span>
                  </div>
                )}
              />
              <Menu.Content className="media-menu__panel">
                <Menu.Item className="media-menu__back">
                  <MenuChevron flipped />
                  {t(captionsText)}
                </Menu.Item>
                <Menu.Separator className="media-menu__separator" />
                <CaptionsRadioGroup
                  className="media-menu__group"
                  aria-label={t(captionsText)}
                  renderItem={(props, item) => (
                    <Menu.RadioItem {...props} className="media-menu__item">
                      <bdi dir="auto">{item.label}</bdi>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}
        </Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

// ================================================================
// Player
// ================================================================

const SEEK_TIME = 10;

export interface VideoPlayerProps {
  src: string;
  poster?: string | undefined;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

/**
 * @example
 * ```tsx
 * <VideoPlayer
 *   src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
 *   poster="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp"
 * />
 * ```
 */
export function VideoPlayer({ src, poster, className, renderPoster, style, ...rest }: VideoPlayerProps): ReactNode {
  return (
    <Player poster={poster}>
      <Container className={`media-minimal-skin media-minimal-skin--video ${className ?? ''}`} style={style} {...rest}>
        <Video src={src} playsInline />

        <Poster render={renderPoster} />

        <BufferingIndicator
          render={(props) => (
            <div {...props} className="media-buffering-indicator">
              <SpinnerIcon className="media-icon" />
            </div>
          )}
        />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--primary"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <Controls.Root>
          <Controls.Backdrop className="media-overlay" />
          <Controls.Content className="media-controls">
            <Tooltip.Provider>
              <div className="media-button-group">
                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <VolumePopover />
              </div>

              <div className="media-time-controls">
                <Time.Group className="media-time-group">
                  <Time.Value toggle type="current" className="media-time media-time--current" />
                  <Time.Separator className="media-time-separator" />
                  <Time.Value type="duration" className="media-time media-time--duration" />
                </Time.Group>

                <TimeSlider.Root className="media-slider">
                  <TimeSlider.Chapters
                    className="media-slider__chapters"
                    renderChapter={(props) => (
                      <div {...props} className={`${props.className} media-slider__chapter`}>
                        <TimeSlider.Track className="media-slider__track media-slider__chapter-track">
                          <TimeSlider.Buffer className="media-slider__buffer" />
                          <TimeSlider.Fill className="media-slider__fill" />
                        </TimeSlider.Track>
                      </div>
                    )}
                  />
                  <TimeSlider.Thumb className="media-slider__thumb" />

                  <TimeSlider.Preview className="media-slider__preview">
                    <div className="media-thumbnail media-slider__thumbnail">
                      <Slider.Thumbnail className="media-thumbnail__image" />
                      <SpinnerIcon className="media-thumbnail__spinner media-icon" />
                    </div>
                    <div className="media-slider__value">
                      <TimeSlider.ChapterTitle className="media-slider__chapter-title" />
                      <TimeSlider.Value type="pointer" className="media-time" />
                    </div>
                  </TimeSlider.Preview>
                </TimeSlider.Root>
              </div>

              <div className="media-button-group">
                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <CaptionsButton className="media-button--captions" render={<Button />}>
                        <CaptionsOffIcon className="media-icon media-icon--captions-off" />
                        <CaptionsOnIcon className="media-icon media-icon--captions-on" />
                      </CaptionsButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <SettingsMenu />

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <CastButton className="media-button--cast" render={<Button />}>
                        <CastEnterIcon className="media-icon media-icon--cast-enter" />
                        <CastExitIcon className="media-icon media-icon--cast-exit" />
                      </CastButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <AirPlayButton className="media-button--airplay" render={<Button />}>
                        <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
                        <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
                      </AirPlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <PiPButton className="media-button--pip" render={<Button />}>
                        <PipEnterIcon className="media-icon media-icon--pip-enter" />
                        <PipExitIcon className="media-icon media-icon--pip-exit" />
                      </PiPButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <FullscreenButton className="media-button--fullscreen" render={<Button />}>
                        <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
                        <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
                      </FullscreenButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>
              </div>
            </Tooltip.Provider>
          </Controls.Content>
        </Controls.Root>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="f" action="toggleFullscreen" />
        <Hotkey keys="c" action="toggleSubtitles" />
        <Hotkey keys="i" action="togglePictureInPicture" />
        <Hotkey keys="ArrowRight" action="seekStep" value={SEEK_TIME / 2} />
        <Hotkey keys="ArrowLeft" action="seekStep" value={-(SEEK_TIME / 2)} />
        <Hotkey keys="l" action="seekStep" value={SEEK_TIME} />
        <Hotkey keys="j" action="seekStep" value={-SEEK_TIME} />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
        <Hotkey keys="0-9" action="seekToPercent" />
        <Hotkey keys="Home" action="seekToPercent" value={0} />
        <Hotkey keys="End" action="seekToPercent" value={100} />
        <Hotkey keys=">" action="speedUp" />
        <Hotkey keys="<" action="speedDown" />

        {/* Gestures */}
        <Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
        <Gesture type="tap" action="toggleControls" pointer="touch" />
        <Gesture type="doubletap" action="seekStep" value={-SEEK_TIME} region="left" />
        <Gesture type="doubletap" action="toggleFullscreen" region="center" />
        <Gesture type="doubletap" action="seekStep" value={SEEK_TIME} region="right" />

        {/* Input Indicators */}
        <StatusAnnouncer className="media-sr-only" />
        <div className="media-input-indicator-overlay">
          <VolumeIndicator.Root className="media-volume-indicator">
            <VolumeIndicator.Fill className="media-volume-indicator__content">
              <VolumeHighIcon className="media-icon media-icon--volume-high" />
              <VolumeLowIcon className="media-icon media-icon--volume-low" />
              <VolumeOffIcon className="media-icon media-icon--volume-off" />
              <div className="media-volume-indicator__progress" aria-hidden="true" />
              <VolumeIndicator.Value className="media-volume-indicator__value" />
            </VolumeIndicator.Fill>
          </VolumeIndicator.Root>

          <StatusIndicator.Root
            actions={TOP_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--state"
          >
            <div className="media-status-indicator__content">
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
              <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
              <PipEnterIcon className="media-icon media-icon--pip-enter" />
              <PipExitIcon className="media-icon media-icon--pip-exit" />
              <StatusIndicator.Value className="media-status-indicator__value" />
            </div>
          </StatusIndicator.Root>

          <SeekIndicator.Root className="media-seek-indicator">
            <ChevronIcon className="media-icon media-icon--seek" />
            <SeekIndicator.Value className="media-seek-indicator__value" />
          </SeekIndicator.Root>

          <StatusIndicator.Root
            actions={CENTER_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--playback"
          >
            <PlayIcon className="media-icon media-icon--play" />
            <PauseIcon className="media-icon media-icon--pause" />
          </StatusIndicator.Root>
        </div>
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) {
    return (
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={muteButton} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="right">
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={<Popover.Trigger render={muteButton} />} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
      <Popover.Popup className="media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Minimal audio skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { playbackRateText } from '@videojs/core/i18n/text/menu';
import { CheckIcon, PauseIcon, PlayIcon, RestartIcon, SeekIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
import { useTranslator, Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, Menu, MuteButton, PlayButton, usePlaybackRateOptions, PlaybackRateButton, PlaybackRateRadioGroup as PlaybackRateRadioGroupComponent, Popover, SeekButton, StatusAnnouncer, Time, TimeSlider, Tooltip, VolumeSlider, type Poster } from '@videojs/react';
import { Audio } from '@videojs/react/audio';
import './player.css';
import { Player } from './player';

function PlaybackRateRadioGroup(): ReactNode {
  const t = useTranslator();

  return (
    <PlaybackRateRadioGroupComponent
      className="media-menu__group"
      aria-label={t(playbackRateText)}
      renderItem={(props, item) => (
        <Menu.RadioItem {...props} className="media-menu__item">
          <span>{item.label}</span>
          <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
            <CheckIcon className="media-icon" />
          </Menu.ItemIndicator>
        </Menu.RadioItem>
      )}
    />
  );
}

function PlaybackRateTrigger(): ReactNode {
  const state = usePlaybackRateOptions();
  if (!state) return null;

  return (
    <Tooltip.Root side="top" boundary="viewport">
      <Tooltip.Trigger
        render={
          <Menu.Trigger
            disabled={state.disabled}
            render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
          />
        }
      />
      <Tooltip.Popup className="media-tooltip">
        <Tooltip.Label />
        <Tooltip.Shortcut className="media-tooltip__kbd" />
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

// ================================================================
// Player
// ================================================================

export interface AudioPlayerProps {
  src: string;
  style?: CSSProperties;
  className?: string;
}

const SEEK_TIME = 10;

/**
 * @example
 * ```tsx
 * <AudioPlayer
 *   src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
 * />
 * ```
 */
export function AudioPlayer({ src, className, ...rest }: AudioPlayerProps): ReactNode {
  return (
    <Player>
      <Container className={`media-minimal-skin media-minimal-skin--audio ${className ?? ''}`} {...rest}>
        <Audio src={src} />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--subtle"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <div className="media-controls">
          <Tooltip.Provider>
            <div className="media-button-group">
              <span className="media-button--play__wrapper">
                <BufferingIndicator
                  render={(props) => (
                    <div {...props} className="media-buffering-indicator">
                      <SpinnerIcon className="media-icon" />
                    </div>
                  )}
                />
                <Tooltip.Root side="top" boundary="viewport">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>
              </span>

              <Tooltip.Root side="top" boundary="viewport">
                <Tooltip.Trigger
                  render={
                    <SeekButton seconds={-SEEK_TIME} className="media-button--seek" render={<Button />}>
                      <span className="media-icon__container">
                        <SeekIcon className="media-icon media-icon--seek media-icon--flipped" />
                        <span className="media-icon__label">{SEEK_TIME}</span>
                      </span>
                    </SeekButton>
                  }
                />
                <Tooltip.Popup className="media-tooltip">
                  <Tooltip.Label />
                  <Tooltip.Shortcut className="media-tooltip__kbd" />
                </Tooltip.Popup>
              </Tooltip.Root>

              <Tooltip.Root side="top" boundary="viewport">
                <Tooltip.Trigger
                  render={
                    <SeekButton seconds={SEEK_TIME} className="media-button--seek" render={<Button />}>
                      <span className="media-icon__container">
                        <SeekIcon className="media-icon media-icon--seek" />
                        <span className="media-icon__label">{SEEK_TIME}</span>
                      </span>
                    </SeekButton>
                  }
                />
                <Tooltip.Popup className="media-tooltip">
                  <Tooltip.Label />
                  <Tooltip.Shortcut className="media-tooltip__kbd" />
                </Tooltip.Popup>
              </Tooltip.Root>
            </div>

            <div className="media-time-controls">
              <Time.Group className="media-time-group">
                <Time.Value toggle type="current" className="media-time media-time--current" />
                <Time.Separator className="media-time-separator" />
                <Time.Value type="duration" className="media-time media-time--duration" />
              </Time.Group>

              <TimeSlider.Root className="media-slider">
                <TimeSlider.Track className="media-slider__track">
                  <TimeSlider.Buffer className="media-slider__buffer" />
                  <TimeSlider.Fill className="media-slider__fill" />
                </TimeSlider.Track>
                <TimeSlider.Thumb className="media-slider__thumb" />
                <TimeSlider.Preview className="media-slider__preview">
                  <TimeSlider.Value type="pointer" className="media-tooltip media-slider__value media-time" />
                </TimeSlider.Preview>
              </TimeSlider.Root>
            </div>

            <div className="media-button-group">
              <VolumePopover />

              <Menu.Root side="top" align="center" boundary="viewport">
                <PlaybackRateTrigger />
                <Menu.Popup className="media-popover media-menu media-menu--playback-rate">
                  <Menu.Content className="media-menu__content">
                    <PlaybackRateRadioGroup />
                  </Menu.Content>
                </Menu.Popup>
              </Menu.Root>
            </div>
          </Tooltip.Provider>
        </div>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="ArrowRight" action="seekStep" value={5} />
        <Hotkey keys="ArrowLeft" action="seekStep" value={-5} />
        <Hotkey keys="l" action="seekStep" value={10} />
        <Hotkey keys="j" action="seekStep" value={-10} />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
        <Hotkey keys="0-9" action="seekToPercent" />
        <Hotkey keys="Home" action="seekToPercent" value={0} />
        <Hotkey keys="End" action="seekToPercent" value={100} />
        <Hotkey keys=">" action="speedUp" />
        <Hotkey keys="<" action="speedDown" />

        {/* Input Feedback */}
        <StatusAnnouncer className="media-sr-only" />
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) {
    return (
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={muteButton} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="left" boundary="viewport">
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={<Popover.Trigger render={muteButton} />} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
      <Popover.Popup className="media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Default live video skin

The live skins swap the duration and current-time displays for a Live button. They build on the live presets, so the player reports live-edge state.

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { captionsText } from '@videojs/core/i18n/text/menu';
import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, FullscreenEnterIcon, FullscreenExitIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
import { Poster, useTranslator, Container, usePlayer, AirPlayButton, BufferingIndicator, CaptionsButton, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, LiveButton, Menu, MuteButton, PiPButton, PlayButton, Popover, StatusAnnouncer, StatusIndicator, Tooltip, VolumeIndicator, VolumeSlider } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import './player.css';
import { Player } from './player';

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;

function CaptionsTrigger(): ReactNode {
  const t = useTranslator();
  const captions = useCaptionsOptions();
  if (!captions) return null;

  const { disabled } = captions;

  if (!captions.showMenu) {
    return (
      <Tooltip.Root side="top">
        <Tooltip.Trigger
          render={
            <CaptionsButton className="media-button--captions" render={<Button />}>
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
            </CaptionsButton>
          }
        />
        <Tooltip.Popup className="media-surface media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  const { options, setValue, value } = captions;

  return (
    <Menu.Root side="top" align="center">
      <Menu.Trigger
        disabled={disabled}
        render={
          <CaptionsButton className="media-button--captions" render={<Button />}>
            <CaptionsOffIcon className="media-icon media-icon--captions-off" />
            <CaptionsOnIcon className="media-icon media-icon--captions-on" />
          </CaptionsButton>
        }
      />
      <Menu.Popup className="media-surface media-popover media-menu media-menu--captions">
        <Menu.Content className="media-menu__content">
          <Menu.RadioGroup
            className="media-menu__group"
            value={value}
            onValueChange={setValue}
            aria-label={t(captionsText)}
          >
            {options.map((option) => (
              <Menu.RadioItem
                key={option.value}
                className="media-menu__item"
                value={option.value}
                disabled={option.disabled}
              >
                <bdi dir="auto">{option.label}</bdi>
                <Menu.ItemIndicator checked={option.value === value} forceMount className="media-menu__indicator">
                  <CheckIcon className="media-icon" />
                </Menu.ItemIndicator>
              </Menu.RadioItem>
            ))}
          </Menu.RadioGroup>
        </Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

// ================================================================
// Player
// ================================================================

export interface LiveVideoPlayerProps {
  src: string;
  poster?: string | undefined;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

/**
 * @example
 * ```tsx
 * <LiveVideoPlayer
 *   src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8"
 *   poster="https://image.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM/thumbnail.webp"
 * />
 * ```
 */
export function LiveVideoPlayer({ src, poster, className, renderPoster, style, ...rest }: LiveVideoPlayerProps): ReactNode {
  return (
    <Player poster={poster}>
      <Container className={`media-default-skin media-default-skin--video ${className ?? ''}`} style={style} {...rest}>
        <HlsJsVideo src={src} playsInline />

        <Poster render={renderPoster} />

        <BufferingIndicator
          render={(props) => (
            <div {...props} className="media-buffering-indicator">
              <SpinnerIcon className="media-icon" />
            </div>
          )}
        />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog media-surface">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--primary"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <Controls.Root>
          <Controls.Backdrop className="media-overlay" />
          <Controls.Content className="media-surface media-controls media-controls--root">
            <Tooltip.Provider>
              <div className="media-surface media-controls media-controls--primary">
                <div className="media-button-group">
                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <PlayButton className="media-button--play" render={<Button />}>
                          <RestartIcon className="media-icon media-icon--restart" />
                          <PlayIcon className="media-icon media-icon--play" />
                          <PauseIcon className="media-icon media-icon--pause" />
                        </PlayButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <LiveButton className="media-button media-button--subtle media-button--live" />
                </div>

                <div className="media-time-controls" aria-hidden="true" />

                <div className="media-button-group">
                  <VolumePopover />

                  <CaptionsTrigger />

                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <CastButton className="media-button--cast" render={<Button />}>
                          <CastEnterIcon className="media-icon media-icon--cast-enter" />
                          <CastExitIcon className="media-icon media-icon--cast-exit" />
                        </CastButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <AirPlayButton className="media-button--airplay" render={<Button />}>
                          <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
                          <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
                        </AirPlayButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <PiPButton className="media-button--pip" render={<Button />}>
                          <PipEnterIcon className="media-icon media-icon--pip-enter" />
                          <PipExitIcon className="media-icon media-icon--pip-exit" />
                        </PiPButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>

                  <Tooltip.Root side="top">
                    <Tooltip.Trigger
                      render={
                        <FullscreenButton className="media-button--fullscreen" render={<Button />}>
                          <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
                          <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
                        </FullscreenButton>
                      }
                    />
                    <Tooltip.Popup className="media-surface media-tooltip">
                      <Tooltip.Label />
                      <Tooltip.Shortcut className="media-tooltip__kbd" />
                    </Tooltip.Popup>
                  </Tooltip.Root>
                </div>
              </div>
            </Tooltip.Provider>
          </Controls.Content>
        </Controls.Root>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="f" action="toggleFullscreen" />
        <Hotkey keys="c" action="toggleSubtitles" />
        <Hotkey keys="i" action="togglePictureInPicture" />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />

        {/* Gestures */}
        <Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
        <Gesture type="tap" action="toggleControls" pointer="touch" />
        <Gesture type="doubletap" action="toggleFullscreen" region="center" />

        {/* Input Indicators */}
        <StatusAnnouncer className="media-sr-only" />
        <div className="media-input-indicator-overlay">
          <VolumeIndicator.Root className="media-surface media-volume-indicator">
            <VolumeIndicator.Fill className="media-volume-indicator__content">
              <VolumeHighIcon className="media-icon media-icon--volume-high" />
              <VolumeLowIcon className="media-icon media-icon--volume-low" />
              <VolumeOffIcon className="media-icon media-icon--volume-off" />
              <VolumeIndicator.Value className="media-volume-indicator__value" />
            </VolumeIndicator.Fill>
          </VolumeIndicator.Root>

          <StatusIndicator.Root
            actions={TOP_STATUS_ACTIONS}
            className="media-surface media-status-indicator media-status-indicator--state"
          >
            <div className="media-status-indicator__content">
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
              <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
              <PipEnterIcon className="media-icon media-icon--pip-enter" />
              <PipExitIcon className="media-icon media-icon--pip-exit" />
              <StatusIndicator.Value className="media-status-indicator__value" />
            </div>
          </StatusIndicator.Root>

          <StatusIndicator.Root
            actions={CENTER_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--playback"
          >
            <PlayIcon className="media-icon media-icon--play" />
            <PauseIcon className="media-icon media-icon--pause" />
          </StatusIndicator.Root>
        </div>
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) return muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="top">
      <Popover.Trigger render={muteButton} />
      <Popover.Popup className="media-surface media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Default live audio skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { PauseIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons';
import { Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, LiveButton, MuteButton, PlayButton, Popover, StatusAnnouncer, Tooltip, VolumeSlider, type Poster } from '@videojs/react';
import { MuxAudio } from '@videojs/react/media/mux-audio';
import './player.css';
import { Player } from './player';

export function TooltipPopup(props: Omit<Tooltip.PopupProps, 'children' | 'className'>): ReactNode {
  return (
    <Tooltip.Popup className="media-surface media-tooltip" {...props}>
      <Tooltip.Label />
      <Tooltip.Shortcut className="media-tooltip__kbd" />
    </Tooltip.Popup>
  );
}

// ================================================================
// Player
// ================================================================

export interface LiveAudioPlayerProps {
  src: string;
  style?: CSSProperties;
  className?: string;
}

/**
 * @example
 * ```tsx
 * <LiveAudioPlayer
 *   src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8"
 * />
 * ```
 */
export function LiveAudioPlayer({ src, className, ...rest }: LiveAudioPlayerProps): ReactNode {
  return (
    <Player>
      <Container className={`media-default-skin media-default-skin--audio ${className ?? ''}`} {...rest}>
        <MuxAudio src={src} />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--subtle"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <div className="media-surface media-controls">
          <Tooltip.Provider>
            <div className="media-button-group">
              <span className="media-button--play__wrapper">
                <BufferingIndicator
                  render={(props) => (
                    <div {...props} className="media-buffering-indicator">
                      <SpinnerIcon className="media-icon" />
                    </div>
                  )}
                />
                <Tooltip.Root side="top" boundary="viewport">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <TooltipPopup />
                </Tooltip.Root>
              </span>

              <LiveButton className="media-button media-button--subtle media-button--live" />
            </div>

            <div className="media-time-controls" aria-hidden="true" />

            <div className="media-button-group">
              <VolumePopover />
            </div>
          </Tooltip.Provider>
        </div>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />

        {/* Input Feedback */}
        <StatusAnnouncer className="media-sr-only" />
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) return muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="top" boundary="viewport">
      <Popover.Trigger render={muteButton} />
      <Popover.Popup className="media-surface media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="vertical" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Minimal live video skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { captionsText } from '@videojs/core/i18n/text/menu';
import { AirPlayEnterIcon, AirPlayExitIcon, CaptionsOffIcon, CaptionsOnIcon, CastEnterIcon, CastExitIcon, CheckIcon, FullscreenEnterIcon, FullscreenExitIcon, PauseIcon, PipEnterIcon, PipExitIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
import { Poster, useTranslator, Container, usePlayer, AirPlayButton, BufferingIndicator, CaptionsButton, useCaptionsOptions, CastButton, Controls, ErrorDialog, FullscreenButton, Gesture, Hotkey, LiveButton, Menu, MuteButton, PiPButton, PlayButton, Popover, StatusAnnouncer, StatusIndicator, Tooltip, VolumeIndicator, VolumeSlider } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import './player.css';
import { Player } from './player';

const TOP_STATUS_ACTIONS = ['toggleSubtitles', 'toggleFullscreen', 'togglePictureInPicture'] as const;

const CENTER_STATUS_ACTIONS = ['togglePaused'] as const;

function CaptionsTrigger(): ReactNode {
  const t = useTranslator();
  const captions = useCaptionsOptions();
  if (!captions) return null;

  const { disabled } = captions;

  if (!captions.showMenu) {
    return (
      <Tooltip.Root side="top">
        <Tooltip.Trigger
          render={
            <CaptionsButton className="media-button--captions" render={<Button />}>
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
            </CaptionsButton>
          }
        />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  return (
    <Menu.Root side="top" align="center">
      <Menu.Trigger
        disabled={disabled}
        render={
          <CaptionsButton className="media-button--captions" render={<Button />}>
            <CaptionsOffIcon className="media-icon media-icon--captions-off" />
            <CaptionsOnIcon className="media-icon media-icon--captions-on" />
          </CaptionsButton>
        }
      />
      <Menu.Popup className="media-popover media-menu media-menu--captions">
        <Menu.Content className="media-menu__content">
          <Menu.RadioGroup
            className="media-menu__group"
            value={captions.value}
            onValueChange={captions.setValue}
            aria-label={t(captionsText)}
          >
            {captions.options.map((option) => (
              <Menu.RadioItem
                key={option.value}
                className="media-menu__item"
                value={option.value}
                disabled={option.disabled}
              >
                <bdi dir="auto">{option.label}</bdi>
                <Menu.ItemIndicator
                  checked={option.value === captions.value}
                  forceMount
                  className="media-menu__indicator"
                >
                  <CheckIcon className="media-icon" />
                </Menu.ItemIndicator>
              </Menu.RadioItem>
            ))}
          </Menu.RadioGroup>
        </Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

// ================================================================
// Player
// ================================================================

export interface LiveVideoPlayerProps {
  src: string;
  poster?: string | undefined;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

/**
 * @example
 * ```tsx
 * <LiveVideoPlayer
 *   src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8"
 *   poster="https://image.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM/thumbnail.webp"
 * />
 * ```
 */
export function LiveVideoPlayer({ src, poster, className, renderPoster, style, ...rest }: LiveVideoPlayerProps): ReactNode {
  return (
    <Player poster={poster}>
      <Container className={`media-minimal-skin media-minimal-skin--video ${className ?? ''}`} style={style} {...rest}>
        <HlsJsVideo src={src} playsInline />

        <Poster render={renderPoster} />

        <BufferingIndicator
          render={(props) => (
            <div {...props} className="media-buffering-indicator">
              <SpinnerIcon className="media-icon" />
            </div>
          )}
        />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--primary"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <Controls.Root>
          <Controls.Backdrop className="media-overlay" />
          <Controls.Content className="media-controls">
            <Tooltip.Provider>
              <div className="media-button-group">
                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <LiveButton className="media-button media-button--subtle media-button--live" />

                <VolumePopover />
              </div>

              <div className="media-time-controls" aria-hidden="true" />

              <div className="media-button-group">
                <CaptionsTrigger />

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <CastButton className="media-button--cast" render={<Button />}>
                        <CastEnterIcon className="media-icon media-icon--cast-enter" />
                        <CastExitIcon className="media-icon media-icon--cast-exit" />
                      </CastButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <AirPlayButton className="media-button--airplay" render={<Button />}>
                        <AirPlayEnterIcon className="media-icon media-icon--airplay-enter" />
                        <AirPlayExitIcon className="media-icon media-icon--airplay-exit" />
                      </AirPlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <PiPButton className="media-button--pip" render={<Button />}>
                        <PipEnterIcon className="media-icon media-icon--pip-enter" />
                        <PipExitIcon className="media-icon media-icon--pip-exit" />
                      </PiPButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>

                <Tooltip.Root side="top">
                  <Tooltip.Trigger
                    render={
                      <FullscreenButton className="media-button--fullscreen" render={<Button />}>
                        <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
                        <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
                      </FullscreenButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>
              </div>
            </Tooltip.Provider>
          </Controls.Content>
        </Controls.Root>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="f" action="toggleFullscreen" />
        <Hotkey keys="c" action="toggleSubtitles" />
        <Hotkey keys="i" action="togglePictureInPicture" />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />

        {/* Gestures */}
        <Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
        <Gesture type="tap" action="toggleControls" pointer="touch" />
        <Gesture type="doubletap" action="toggleFullscreen" region="center" />

        {/* Input Indicators */}
        <StatusAnnouncer className="media-sr-only" />
        <div className="media-input-indicator-overlay">
          <VolumeIndicator.Root className="media-volume-indicator">
            <VolumeIndicator.Fill className="media-volume-indicator__content">
              <VolumeHighIcon className="media-icon media-icon--volume-high" />
              <VolumeLowIcon className="media-icon media-icon--volume-low" />
              <VolumeOffIcon className="media-icon media-icon--volume-off" />
              <div className="media-volume-indicator__progress" aria-hidden="true" />
              <VolumeIndicator.Value className="media-volume-indicator__value" />
            </VolumeIndicator.Fill>
          </VolumeIndicator.Root>

          <StatusIndicator.Root
            actions={TOP_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--state"
          >
            <div className="media-status-indicator__content">
              <CaptionsOnIcon className="media-icon media-icon--captions-on" />
              <CaptionsOffIcon className="media-icon media-icon--captions-off" />
              <FullscreenEnterIcon className="media-icon media-icon--fullscreen-enter" />
              <FullscreenExitIcon className="media-icon media-icon--fullscreen-exit" />
              <PipEnterIcon className="media-icon media-icon--pip-enter" />
              <PipExitIcon className="media-icon media-icon--pip-exit" />
              <StatusIndicator.Value className="media-status-indicator__value" />
            </div>
          </StatusIndicator.Root>

          <StatusIndicator.Root
            actions={CENTER_STATUS_ACTIONS}
            className="media-status-indicator media-status-indicator--playback"
          >
            <PlayIcon className="media-icon media-icon--play" />
            <PauseIcon className="media-icon media-icon--pause" />
          </StatusIndicator.Root>
        </div>
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) {
    return (
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={muteButton} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="right">
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={<Popover.Trigger render={muteButton} />} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
      <Popover.Popup className="media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}

Minimal live audio skin

'use client';

import { type CSSProperties, type ComponentProps, forwardRef, type ReactNode } from 'react';
import { PauseIcon, PlayIcon, RestartIcon, SpinnerIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/react/icons/minimal';
import { Container, usePlayer, BufferingIndicator, ErrorDialog, Hotkey, LiveButton, MuteButton, PlayButton, Popover, StatusAnnouncer, Tooltip, VolumeSlider, type Poster } from '@videojs/react';
import { MuxAudio } from '@videojs/react/media/mux-audio';
import './player.css';
import { Player } from './player';

// ================================================================
// Player
// ================================================================

export interface LiveAudioPlayerProps {
  src: string;
  style?: CSSProperties;
  className?: string;
}

/**
 * @example
 * ```tsx
 * <LiveAudioPlayer
 *   src="https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8"
 * />
 * ```
 */
export function LiveAudioPlayer({ src, className, ...rest }: LiveAudioPlayerProps): ReactNode {
  return (
    <Player>
      <Container className={`media-minimal-skin media-minimal-skin--audio ${className ?? ''}`} {...rest}>
        <MuxAudio src={src} />

        <ErrorDialog.Root>
          <ErrorDialog.Backdrop className="media-overlay media-error-backdrop" />
          <ErrorDialog.Popup className="media-error">
            <div className="media-error__dialog">
              <div className="media-error__content">
                <ErrorDialog.Title className="media-error__title"></ErrorDialog.Title>
                <ErrorDialog.Description className="media-error__description" />
              </div>
              <div className="media-error__actions">
                <ErrorDialog.Close className="media-button media-button--subtle"></ErrorDialog.Close>
              </div>
            </div>
          </ErrorDialog.Popup>
        </ErrorDialog.Root>

        <div className="media-controls">
          <Tooltip.Provider>
            <div className="media-button-group">
              <span className="media-button--play__wrapper">
                <BufferingIndicator
                  render={(props) => (
                    <div {...props} className="media-buffering-indicator">
                      <SpinnerIcon className="media-icon" />
                    </div>
                  )}
                />
                <Tooltip.Root side="top" boundary="viewport">
                  <Tooltip.Trigger
                    render={
                      <PlayButton className="media-button--play" render={<Button />}>
                        <RestartIcon className="media-icon media-icon--restart" />
                        <PlayIcon className="media-icon media-icon--play" />
                        <PauseIcon className="media-icon media-icon--pause" />
                      </PlayButton>
                    }
                  />
                  <Tooltip.Popup className="media-tooltip">
                    <Tooltip.Label />
                    <Tooltip.Shortcut className="media-tooltip__kbd" />
                  </Tooltip.Popup>
                </Tooltip.Root>
              </span>

              <LiveButton className="media-button media-button--subtle media-button--live" />
            </div>

            <div className="media-time-controls" aria-hidden="true" />

            <div className="media-button-group">
              <VolumePopover />
            </div>
          </Tooltip.Provider>
        </div>

        {/* Hotkeys */}
        <Hotkey keys="Space" action="togglePaused" />
        <Hotkey keys="k" action="togglePaused" />
        <Hotkey keys="m" action="toggleMuted" />
        <Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
        <Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />

        {/* Input Feedback */}
        <StatusAnnouncer className="media-sr-only" />
      </Container>

    </Player>
  );
}

// ================================================================
// Components
// ================================================================

const Button = forwardRef<HTMLButtonElement, ComponentProps<'button'>>(function Button({ className, ...props }, ref) {
  return (
    <button
      ref={ref}
      type="button"
      className={`media-button media-button--subtle media-button--icon ${className ?? ''}`}
      {...props}
    />
  );
});

function VolumePopover(): ReactNode {
  const volumeUnavailable = usePlayer((s) => s.volumeAvailability !== 'available');

  const muteButton = (
    <MuteButton className="media-button--mute" render={<Button />}>
      <VolumeOffIcon className="media-icon media-icon--volume-off" />
      <VolumeLowIcon className="media-icon media-icon--volume-low" />
      <VolumeHighIcon className="media-icon media-icon--volume-high" />
    </MuteButton>
  );

  if (volumeUnavailable) {
    return (
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={muteButton} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
    );
  }

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="left" boundary="viewport">
      <Tooltip.Root side="top" delay={0} sticky>
        <Tooltip.Trigger render={<Popover.Trigger render={muteButton} />} />
        <Tooltip.Popup className="media-tooltip">
          <Tooltip.Label />
          <Tooltip.Shortcut className="media-tooltip__kbd" />
        </Tooltip.Popup>
      </Tooltip.Root>
      <Popover.Popup className="media-popover media-popover--volume">
        <VolumeSlider.Root className="media-slider" orientation="horizontal" thumbAlignment="edge">
          <VolumeSlider.Track className="media-slider__track">
            <VolumeSlider.Fill className="media-slider__fill" />
          </VolumeSlider.Track>
          <VolumeSlider.Thumb className="media-slider__thumb media-slider__thumb--persistent" />
        </VolumeSlider.Root>
      </Popover.Popup>
    </Popover.Root>
  );
}