Skip to content
FrameworkStyle

Customize skins

Learn how to customize Video.js v10 skins by copying and modifying them

Video.js v10 comes with two pre-built skins; Default and Minimal. Basic customization is possible with CSS custom properties, but if you want more control over the design and functionality of your player, you can copy the skin’s code into your project and modify it as needed. We call this “ejecting” the skin, since you’re taking the internal code that makes up the skin and making it your own.

Basic customization

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 Vanilla CSS skins <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 skins use --media-scale-unit as the base for control spacing, icon sizes, and font sizes. It defaults to 16px, so the player UI does not change with the document root font size. 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 of course also add your own classnames to the skins themselves.

Ejecting

If you’d like to customize them you can fully customize them by “ejecting” the code and making it your own.

While eventually we’ll have a CLI that will eject skins in your preferred framework and style, for now we invite you to try it out with these copy-paste-ready implementations.

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.Content className="media-surface media-popover media-menu media-menu--settings">
        <div className="media-menu__group">
          {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">
                      <span className="media-menu__hint-label">{quality.selectedLabel}</span>
                      <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">
                      <span>
                        {item.label}
                        {item.tier ? <sup className="media-menu__tier">{item.tier}</sup> : null}
                      </span>
                      {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">
                      <span className="media-menu__hint-label">{audioTrack.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <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">
                      <span className="media-menu__hint-label">{playbackRate.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <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">
                      <span className="media-menu__hint-label">{captions.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}
        </div>
      </Menu.Content>
    </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.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 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.Root>

        <div className="media-overlay" />

        {/* 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 (
    <Menu.Trigger
      disabled={state.disabled}
      render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
    />
  );
}

// ================================================================
// 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.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-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.Content className="media-surface media-popover media-menu media-menu--playback-rate">
                  <PlaybackRateRadioGroup />
                </Menu.Content>
              </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.Content className="media-popover media-menu media-menu--settings">
        <div className="media-menu__group">
          {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">
                      <span className="media-menu__hint-label">{quality.selectedLabel}</span>
                      <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">
                      <span>
                        {item.label}
                        {item.tier ? <sup className="media-menu__tier">{item.tier}</sup> : null}
                      </span>
                      {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">
                      <span className="media-menu__hint-label">{audioTrack.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <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">
                      <span className="media-menu__hint-label">{playbackRate.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <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">
                      <span className="media-menu__hint-label">{captions.selectedLabel}</span>
                      <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">
                      <span>{item.label}</span>
                      <Menu.ItemIndicator checked={item.checked} forceMount className="media-menu__indicator">
                        <CheckIcon className="media-icon" />
                      </Menu.ItemIndicator>
                    </Menu.RadioItem>
                  )}
                />
              </Menu.Content>
            </Menu.Root>
          ) : null}
        </div>
      </Menu.Content>
    </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.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 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.Root>

        <div className="media-overlay" />

        {/* 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 muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="right">
      <Popover.Trigger render={muteButton} />
      <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 (
    <Menu.Trigger
      disabled={state.disabled}
      render={<PlaybackRateButton className="media-button--playback-rate" render={<Button />} />}
    />
  );
}

// ================================================================
// 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.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-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.Content className="media-popover media-menu media-menu--playback-rate">
                  <PlaybackRateRadioGroup />
                </Menu.Content>
              </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 muteButton;

  return (
    <Popover.Root openOnHover delay={200} closeDelay={100} side="left" boundary="viewport">
      <Popover.Trigger render={muteButton} />
      <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>
  );
}