Skip to content
v10 beta

The Open Source Player
for the Web

For over 15 years, Video.js has been the world's web video player. Now rebuilt in v10 for modern development and performance.

0:00

Video hosting sponsored by

Assemble your player

Feel at home with your framework, skin, and media source

import { VideoPlayer, VideoSkin, Video } from '@videojs/react/video';
import '@videojs/react/video/skin.css';

export function App() {
  return (
    <VideoPlayer poster="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.webp">
      <VideoSkin>
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" playsInline />
      </VideoSkin>
    </VideoPlayer>
  );
}

Take full control

Make your player truly your own with fully-editable components

'use client';

import { type SVGProps, type ComponentProps, type ReactElement, type ReactNode, type CSSProperties } from 'react';
import { SpinnerIcon as SpinnerIconPrimitive, ChevronIcon as ChevronIconPrimitive, CaptionsOnIcon as CaptionsOnIconPrimitive, CaptionsOffIcon as CaptionsOffIconPrimitive, FullscreenEnterIcon as FullscreenEnterIconPrimitive, FullscreenExitIcon as FullscreenExitIconPrimitive, PipEnterIcon as PipEnterIconPrimitive, PipExitIcon as PipExitIconPrimitive, PlayIcon as PlayIconPrimitive, PauseIcon as PauseIconPrimitive, VolumeHighIcon as VolumeHighIconPrimitive, VolumeLowIcon as VolumeLowIconPrimitive, VolumeOffIcon as VolumeOffIconPrimitive, AirPlayEnterIcon as AirPlayEnterIconPrimitive, AirPlayExitIcon as AirPlayExitIconPrimitive, CastEnterIcon as CastEnterIconPrimitive, CastExitIcon as CastExitIconPrimitive, RestartIcon as RestartIconPrimitive, CheckIcon as CheckIconPrimitive, GearIcon as GearIconPrimitive } from '@videojs/react/icons';
import { SpinnerIcon as SpinnerIconPrimitive2, ChevronIcon as ChevronIconPrimitive2, CaptionsOnIcon as CaptionsOnIconPrimitive2, CaptionsOffIcon as CaptionsOffIconPrimitive2, FullscreenEnterIcon as FullscreenEnterIconPrimitive2, FullscreenExitIcon as FullscreenExitIconPrimitive2, PipEnterIcon as PipEnterIconPrimitive2, PipExitIcon as PipExitIconPrimitive2, PlayIcon as PlayIconPrimitive2, PauseIcon as PauseIconPrimitive2, VolumeHighIcon as VolumeHighIconPrimitive2, VolumeLowIcon as VolumeLowIconPrimitive2, VolumeOffIcon as VolumeOffIconPrimitive2, AirPlayEnterIcon as AirPlayEnterIconPrimitive2, AirPlayExitIcon as AirPlayExitIconPrimitive2, CastEnterIcon as CastEnterIconPrimitive2, CastExitIcon as CastExitIconPrimitive2, RestartIcon as RestartIconPrimitive2, CheckIcon as CheckIconPrimitive2, GearIcon as GearIconPrimitive2 } from '@videojs/react/icons/minimal';
import { BufferingIndicator as BufferingIndicatorPrimitive, ErrorDialog as ErrorDialogPrimitive, Container as ContainerPrimitive, Poster as PosterPrimitive, Gesture, Hotkey, SeekIndicator as SeekIndicatorPrimitive, StatusAnnouncer as StatusAnnouncerPrimitive, StatusIndicator as StatusIndicatorPrimitive, VolumeIndicator as VolumeIndicatorPrimitive, Tooltip, AirPlayButton as AirPlayButtonPrimitive, CaptionsButton as CaptionsButtonPrimitive, CastButton as CastButtonPrimitive, FullscreenButton as FullscreenButtonPrimitive, PiPButton as PiPButtonPrimitive, PlayButton as PlayButtonPrimitive, MuteButton as MuteButtonPrimitive, VolumeSlider as VolumeSliderPrimitive, VolumePopover as VolumePopoverPrimitive, TimeSlider as TimeSliderPrimitive, Slider, Menu, Text as TextPrimitive, Controls, Time } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './player.css';
import { Player } from './player';
import { type VolumeSliderProps as CoreVolumeSliderProps, type SliderPreviewOverflow } from '@videojs/core';
import { audioText, captionsText, speedText, qualityText, settingsText } from '@videojs/core/i18n/text/menu';
import { type Poster } from '@videojs/react';

type ContainerProps = ContainerPrimitive.Props;

function Container({ children, className, ...props }: ContainerProps) {
  return (
    <ContainerPrimitive className={`media-container ${className ?? ''}`} {...props}>
      {children}
    </ContainerPrimitive>
  );
}

interface PosterProps extends Omit<PosterPrimitive.Props, 'children' | 'render'> {
  children?: PosterPrimitive.Props['render'];
}

function Poster({ children, className, src, ...props }: PosterProps = {}) {
  return (
    <PosterPrimitive
      render={children}
      className={(state) => `media-poster ${resolveClassName(className, state)}`}
      src={src}
      {...props}
    />
  );
}

interface DefaultVideoSkinProps extends Omit<NonNullable<ComponentProps<typeof Container>>, 'children'> {
  src: string;
  poster?: string | undefined;
  poster?: string | NonNullable<ComponentProps<typeof Poster>>['children'];
}

type BufferingIndicatorProps = Omit<BufferingIndicatorPrimitive.Props, 'children'>;

function BufferingIndicator({ className, ...props }: BufferingIndicatorProps = {}) {
  return (
    <BufferingIndicatorPrimitive
      className={(state) => `media-buffering-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <>
        <span className="contents theme-minimal:hidden">
          <SpinnerIconPrimitive className={'media-buffering-indicator-spinner-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <SpinnerIconPrimitive2 className={'media-buffering-indicator-spinner-icon'} />
        </span>
      </>
    </BufferingIndicatorPrimitive>
  );
}

type ErrorDialogProps = Omit<ErrorDialogPrimitive.PopupProps, 'children'>;

type ButtonProps = ComponentProps<'button'>;

type PlayButtonProps = Omit<PlayButtonPrimitive.Props, 'children'>;

interface ButtonTooltipProps extends Tooltip.RootProps {
  children: ReactElement;
  label?: ReactNode;
}

function ButtonTooltip({ children, label, ...props }: ButtonTooltipProps) {
  return (
    <Tooltip.Root {...props}>
      <Tooltip.Trigger render={children} />
      <Tooltip.Popup className={'media-tooltip'}>
        {label ?? <Tooltip.Label />}
        {!label && <Tooltip.Shortcut className={'media-tooltip-shortcut'} />}
      </Tooltip.Popup>
    </Tooltip.Root>
  );
}

function PlayButton({ className, ...props }: PlayButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <PlayButtonPrimitive
        render={<Button />}
        className={(state) => `media-play-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <RestartIconPrimitive className={'media-button-icon media-play-button-restart-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <RestartIconPrimitive2 className={'media-button-icon media-play-button-restart-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <PlayIconPrimitive className={'media-button-icon media-play-button-play-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PlayIconPrimitive2 className={'media-button-icon media-play-button-play-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <PauseIconPrimitive className={'media-button-icon media-play-button-pause-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PauseIconPrimitive2 className={'media-button-icon media-play-button-pause-icon'} />
          </span>
        </>
      </PlayButtonPrimitive>
    </ButtonTooltip>
  );
}

interface VolumePopoverProps extends Omit<VolumePopoverPrimitive.RootProps, 'children'> {
  className?: string | undefined;
  orientation?: CoreVolumeSliderProps['orientation'];
  showTooltip?: boolean;
  popupClassName?: ClassValue;
}

type MuteButtonProps = Omit<MuteButtonPrimitive.Props, 'children'>;

function MuteButton({ className, ...props }: MuteButtonProps = {}) {
  return (
    <MuteButtonPrimitive
      render={<Button />}
      className={(state) => `media-mute-button ${resolveClassName(className, state)}`}
      {...props}
    >
      <>
        <span className="contents theme-minimal:hidden">
          <VolumeOffIconPrimitive className={'media-button-icon media-mute-button-off-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <VolumeOffIconPrimitive2 className={'media-button-icon media-mute-button-off-icon'} />
        </span>
      </>
      <>
        <span className="contents theme-minimal:hidden">
          <VolumeLowIconPrimitive className={'media-button-icon media-mute-button-low-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <VolumeLowIconPrimitive2 className={'media-button-icon media-mute-button-low-icon'} />
        </span>
      </>
      <>
        <span className="contents theme-minimal:hidden">
          <VolumeHighIconPrimitive className={'media-button-icon media-mute-button-high-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <VolumeHighIconPrimitive2 className={'media-button-icon media-mute-button-high-icon'} />
        </span>
      </>
    </MuteButtonPrimitive>
  );
}

type VolumeSliderProps = Omit<VolumeSliderPrimitive.RootProps, 'children'>;

function VolumeSlider({ className, ...props }: VolumeSliderProps = {}) {
  return (
    <VolumeSliderPrimitive.Root
      className={(state) => `media-slider media-volume-slider ${resolveClassName(className, state)}`}
      thumbAlignment="edge"
      {...props}
    >
      <VolumeSliderPrimitive.Track render={<SliderTrack />}>
        <VolumeSliderPrimitive.Fill render={<SliderFill />} />
      </VolumeSliderPrimitive.Track>
      <VolumeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-volume-slider-thumb'} />
    </VolumeSliderPrimitive.Root>
  );
}

interface TimeSliderProps extends Omit<TimeSliderPrimitive.RootProps, 'children'> {
  previewOverflow?: SliderPreviewOverflow | undefined;
}

function TimeSlider({ className, previewOverflow = 'visible', ...props }: TimeSliderProps = {}) {
  return (
    <TimeSliderPrimitive.Root
      className={(state) => `media-slider media-time-slider ${resolveClassName(className, state)}`}
      {...props}
    >
      <TimeSliderPrimitive.Chapters
        className={'media-time-slider-chapters'}
        renderChapter={(props) => (
          <div className={'media-time-slider-chapter'} {...props}>
            <TimeSliderPrimitive.Track render={<SliderTrack />} className={'media-time-slider-chapter-track'}>
              <TimeSliderPrimitive.Buffer render={<SliderBuffer />} />
              <TimeSliderPrimitive.Fill render={<SliderFill />} />
            </TimeSliderPrimitive.Track>
          </div>
        )}
      ></TimeSliderPrimitive.Chapters>
      <TimeSliderPrimitive.Thumb render={<SliderThumb />} className={'media-time-slider-thumb'} />
      <TimeSliderPrimitive.Preview className={'media-slider-preview'} overflow={previewOverflow}>
        <div className={'media-slider-thumbnail'}>
          <Slider.Thumbnail className={'media-slider-thumbnail-image'} />
          <>
            <span className="contents theme-minimal:hidden">
              <SpinnerIconPrimitive className={'media-slider-thumbnail-spinner-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <SpinnerIconPrimitive2 className={'media-slider-thumbnail-spinner-icon'} />
            </span>
          </>
        </div>
        <div className={'media-time-slider-preview-content'}>
          <TimeSliderPrimitive.ChapterTitle className={'media-time-slider-chapter-title'} />
          <TimeSliderPrimitive.Value className={'media-time-slider-value'} type="pointer" />
        </div>
      </TimeSliderPrimitive.Preview>
    </TimeSliderPrimitive.Root>
  );
}

type CaptionsButtonProps = Omit<CaptionsButtonPrimitive.Props, 'children'>;

function CaptionsButton({ className, ...props }: CaptionsButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <CaptionsButtonPrimitive
        render={<Button />}
        className={(state) => `media-captions-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <CaptionsOffIconPrimitive className={'media-button-icon media-captions-button-off-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CaptionsOffIconPrimitive2 className={'media-button-icon media-captions-button-off-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <CaptionsOnIconPrimitive className={'media-button-icon media-captions-button-on-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CaptionsOnIconPrimitive2 className={'media-button-icon media-captions-button-on-icon'} />
          </span>
        </>
      </CaptionsButtonPrimitive>
    </ButtonTooltip>
  );
}

interface SettingsMenuProps extends Menu.RootProps {
  className?: string | undefined;
}

function SettingsMenu({ children, className, ...props }: SettingsMenuProps) {
  return (
    <Menu.Root side="top" align="center" {...props}>
      <ButtonTooltip label={<TextPrimitive token={settingsText.key}>{settingsText.text}</TextPrimitive>} side="top">
        <Menu.Trigger render={<Button />} className={'media-settings-menu-trigger'}>
          <>
            <span className="contents theme-minimal:hidden">
              <GearIconPrimitive className={'media-button-icon media-settings-menu-trigger-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <GearIconPrimitive2 className={'media-button-icon media-settings-menu-trigger-icon'} />
            </span>
          </>
          <TextPrimitive className={'media-settings-menu-trigger-label'} token={settingsText.key}>
            {settingsText.text}
          </TextPrimitive>
        </Menu.Trigger>
      </ButtonTooltip>
      <Menu.Popup keepMounted className={(state) => `media-menu-popup ${resolveClassName(className, state)}`}>
        <Menu.Content className={'media-menu-content'}>{children}</Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

type VideoSettingsMenuProps = Omit<NonNullable<ComponentProps<typeof SettingsMenu>>, 'children'>;

interface QualityMenuProps extends MenuProps {
  className?: ClassValue;
}

interface MenuChevronProps {
  back?: boolean;
  className?: ClassValue;
}

function MenuChevron({ back = false, className }: MenuChevronProps = {}) {
  return (
    <>
      <span className="contents theme-minimal:hidden">
        <ChevronIconPrimitive
          className={`${back ? 'media-menu-back-chevron' : 'media-menu-forward-chevron'} ${className ?? ''}`}
        />
      </span>
      <span className="hidden theme-minimal:contents">
        <ChevronIconPrimitive2
          className={`${back ? 'media-menu-back-chevron' : 'media-menu-forward-chevron'} ${className ?? ''}`}
        />
      </span>
    </>
  );
}

type RadioItemProps = Menu.RadioItemProps;

function RadioItem({ children, className, ...props }: RadioItemProps) {
  return (
    <Menu.RadioItem className={(state) => `media-menu-radio-item ${resolveClassName(className, state)}`} {...props}>
      {children}
      <Menu.ItemIndicator forceMount className={'media-menu-item-indicator'}>
        <>
          <span className="contents theme-minimal:hidden">
            <CheckIconPrimitive className={'media-menu-radio-item-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CheckIconPrimitive2 className={'media-menu-radio-item-icon'} />
          </span>
        </>
      </Menu.ItemIndicator>
    </Menu.RadioItem>
  );
}

function QualityMenu({ className, ...props }: QualityMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <QualityRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <>
            <span className="contents theme-minimal:hidden">
              <SwitchesIconPrimitive className={'media-menu-trigger-item-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <SwitchesIconPrimitive2 className={'media-menu-trigger-item-icon'} />
            </span>
          </>
          <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <QualityRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={(state) => `media-menu-content ${resolveClassName(className, state)}`}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={qualityText.key}>{qualityText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <QualityRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>
                  <span>{item.label}</span>
                  {item.tier ? <sup className={'media-menu-tier'}>{item.tier}</sup> : null}
                </span>
                {item.badge ? <span className={'media-menu-badge'}>{item.badge}</span> : null}
              </RadioItem>
            )}
          ></QualityRadioGroup.Options>
        </Menu.Content>
      </QualityRadioGroup.Root>
    </Menu.Root>
  );
}

interface AudioTrackMenuProps extends MenuProps {
  className?: ClassValue;
}

function AudioTrackMenu({ className, ...props }: AudioTrackMenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <AudioTrackRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <>
            <span className="contents theme-minimal:hidden">
              <SpeechIconPrimitive className={'media-menu-trigger-item-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <SpeechIconPrimitive2 className={'media-menu-trigger-item-icon'} />
            </span>
          </>
          <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <AudioTrackRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={(state) => `media-menu-content ${resolveClassName(className, state)}`}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={audioText.key}>{audioText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <AudioTrackRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></AudioTrackRadioGroup.Options>
        </Menu.Content>
      </AudioTrackRadioGroup.Root>
    </Menu.Root>
  );
}

interface PlaybackRateSubmenuProps extends MenuProps {
  className?: ClassValue;
}

function PlaybackRateSubmenu({ className, ...props }: PlaybackRateSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <PlaybackRateRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <>
            <span className="contents theme-minimal:hidden">
              <SpeedIconPrimitive className={'media-menu-trigger-item-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <SpeedIconPrimitive2 className={'media-menu-trigger-item-icon'} />
            </span>
          </>
          <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <PlaybackRateRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={(state) => `media-menu-content ${resolveClassName(className, state)}`}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={speedText.key}>{speedText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <PlaybackRateRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></PlaybackRateRadioGroup.Options>
        </Menu.Content>
      </PlaybackRateRadioGroup.Root>
    </Menu.Root>
  );
}

interface CaptionsSubmenuProps extends MenuProps {
  className?: ClassValue;
}

function CaptionsSubmenu({ className, ...props }: CaptionsSubmenuProps = {}) {
  return (
    <Menu.Root {...props}>
      <CaptionsRadioGroup.Root>
        <Menu.Trigger className={'media-menu-trigger-item'}>
          <>
            <span className="contents theme-minimal:hidden">
              <CaptionsOffIconPrimitive className={'media-menu-trigger-item-icon'} />
            </span>
            <span className="hidden theme-minimal:contents">
              <CaptionsOffIconPrimitive2 className={'media-menu-trigger-item-icon'} />
            </span>
          </>
          <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          <span className={'media-menu-hint'}>
            <CaptionsRadioGroup.Value className={'media-menu-hint-label'} />
            <MenuChevron />
          </span>
        </Menu.Trigger>
        <Menu.Content className={(state) => `media-menu-content ${resolveClassName(className, state)}`}>
          <Menu.Item className={'media-menu-back-item'}>
            <MenuChevron back />
            <TextPrimitive token={captionsText.key}>{captionsText.text}</TextPrimitive>
          </Menu.Item>
          <Menu.Separator className={'media-menu-separator'} />
          <CaptionsRadioGroup.Options
            className={'media-menu-radio-group'}
            renderItem={(props, item) => (
              <RadioItem {...props}>
                <span>{item.label}</span>
              </RadioItem>
            )}
          ></CaptionsRadioGroup.Options>
        </Menu.Content>
      </CaptionsRadioGroup.Root>
    </Menu.Root>
  );
}

function VideoSettingsMenu(props: VideoSettingsMenuProps = {}) {
  return (
    <SettingsMenu {...props}>
      <QualityMenu />
      <AudioTrackMenu />
      <PlaybackRateSubmenu />
      <CaptionsSubmenu />
    </SettingsMenu>
  );
}

type CastButtonProps = Omit<CastButtonPrimitive.Props, 'children'>;

function CastButton({ className, ...props }: CastButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <CastButtonPrimitive
        render={<Button />}
        className={(state) => `media-cast-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <CastEnterIconPrimitive className={'media-button-icon media-cast-button-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CastEnterIconPrimitive2 className={'media-button-icon media-cast-button-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <CastExitIconPrimitive className={'media-button-icon media-cast-button-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CastExitIconPrimitive2 className={'media-button-icon media-cast-button-exit-icon'} />
          </span>
        </>
      </CastButtonPrimitive>
    </ButtonTooltip>
  );
}

type AirPlayButtonProps = Omit<AirPlayButtonPrimitive.Props, 'children'>;

function AirPlayButton({ className, ...props }: AirPlayButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <AirPlayButtonPrimitive
        render={<Button />}
        className={(state) => `media-airplay-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <AirPlayEnterIconPrimitive className={'media-button-icon media-airplay-button-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <AirPlayEnterIconPrimitive2 className={'media-button-icon media-airplay-button-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <AirPlayExitIconPrimitive className={'media-button-icon media-airplay-button-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <AirPlayExitIconPrimitive2 className={'media-button-icon media-airplay-button-exit-icon'} />
          </span>
        </>
      </AirPlayButtonPrimitive>
    </ButtonTooltip>
  );
}

type PiPButtonProps = Omit<PiPButtonPrimitive.Props, 'children'>;

function PiPButton({ className, ...props }: PiPButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <PiPButtonPrimitive
        render={<Button />}
        className={(state) => `media-pip-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <PipEnterIconPrimitive className={'media-button-icon media-pip-button-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PipEnterIconPrimitive2 className={'media-button-icon media-pip-button-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <PipExitIconPrimitive className={'media-button-icon media-pip-button-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PipExitIconPrimitive2 className={'media-button-icon media-pip-button-exit-icon'} />
          </span>
        </>
      </PiPButtonPrimitive>
    </ButtonTooltip>
  );
}

type FullscreenButtonProps = Omit<FullscreenButtonPrimitive.Props, 'children'>;

function FullscreenButton({ className, ...props }: FullscreenButtonProps = {}) {
  return (
    <ButtonTooltip side="top">
      <FullscreenButtonPrimitive
        render={<Button />}
        className={(state) => `media-fullscreen-button ${resolveClassName(className, state)}`}
        {...props}
      >
        <>
          <span className="contents theme-minimal:hidden">
            <FullscreenEnterIconPrimitive className={'media-button-icon media-fullscreen-button-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <FullscreenEnterIconPrimitive2 className={'media-button-icon media-fullscreen-button-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <FullscreenExitIconPrimitive className={'media-button-icon media-fullscreen-button-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <FullscreenExitIconPrimitive2 className={'media-button-icon media-fullscreen-button-exit-icon'} />
          </span>
        </>
      </FullscreenButtonPrimitive>
    </ButtonTooltip>
  );
}

function DefaultVideoControls() {
  return (
    <Controls.Root>
      <Controls.Backdrop className={'media-controls__backdrop'} />
      <Controls.Content className={'media-controls media-controls'}>
        <Tooltip.Provider>
          <Controls.Group className={'media-controls-primary'}>
            <PlayButton />
            <VolumePopover className={'media-controls-volume-button'} />

            <Controls.Group className={'media-time-slider-group'}>
              <Time.Value className={'media-time-current-value'} type="current" />
              <TimeSlider />
              <Time.Value className={'media-time-remaining-value'} type="remaining" toggle />
            </Controls.Group>

            <CaptionsButton className={'media-controls-captions-button'} />
            <VideoSettingsMenu className={'media-controls-settings-button'} />
          </Controls.Group>

          <Controls.Group className={'media-controls-secondary'}>
            <CastButton />
            <AirPlayButton />
            <PiPButton />
            <FullscreenButton />
          </Controls.Group>
        </Tooltip.Provider>
      </Controls.Content>
    </Controls.Root>
  );
}

interface VideoHotkeysProps {
  disabled?: boolean | undefined;
}

interface PlaybackHotkeysProps {
  disabled?: boolean | undefined;
}

function PlaybackHotkeys({ disabled = false }: PlaybackHotkeysProps = {}) {
  return (
    <>
      <Hotkey disabled={disabled} keys="Space" action="togglePaused" />
      <Hotkey disabled={disabled} keys="k" action="togglePaused" />
      <Hotkey disabled={disabled} keys="m" action="toggleMuted" />
      <Hotkey disabled={disabled} keys="ArrowRight" action="seekStep" value={5} />
      <Hotkey disabled={disabled} keys="ArrowLeft" action="seekStep" value={-5} />
      <Hotkey disabled={disabled} keys="l" action="seekStep" value={10} />
      <Hotkey disabled={disabled} keys="j" action="seekStep" value={-10} />
      <Hotkey disabled={disabled} keys="ArrowUp" action="volumeStep" value={0.05} />
      <Hotkey disabled={disabled} keys="ArrowDown" action="volumeStep" value={-0.05} />
      <Hotkey disabled={disabled} keys="0-9" action="seekToPercent" />
      <Hotkey disabled={disabled} keys="Home" action="seekToPercent" value={0} />
      <Hotkey disabled={disabled} keys="End" action="seekToPercent" value={100} />
      <Hotkey disabled={disabled} keys=">" action="speedUp" />
      <Hotkey disabled={disabled} keys="<" action="speedDown" />
    </>
  );
}

function VideoHotkeys({ disabled = false }: VideoHotkeysProps = {}) {
  return (
    <>
      <PlaybackHotkeys disabled={disabled} />
      <Hotkey disabled={disabled} keys="f" action="toggleFullscreen" />
      <Hotkey disabled={disabled} keys="c" action="toggleSubtitles" />
      <Hotkey disabled={disabled} keys="i" action="togglePictureInPicture" />
    </>
  );
}

interface VideoGesturesProps {
  disabled?: boolean | undefined;
}

function VideoGestures({ disabled = false }: VideoGesturesProps = {}) {
  return (
    <>
      <Gesture disabled={disabled} type="tap" action="togglePaused" pointer="mouse" region="center" />
      <Gesture disabled={disabled} type="tap" action="toggleControls" pointer="touch" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" value={-10} region="left" />
      <Gesture disabled={disabled} type="doubletap" action="toggleFullscreen" region="center" />
      <Gesture disabled={disabled} type="doubletap" action="seekStep" value={10} region="right" />
    </>
  );
}

type VideoStatusIndicatorsProps = Omit<ComponentProps<'div'>, 'children'>;

type StatusAnnouncerProps = Omit<StatusAnnouncerPrimitive.Props, 'children'>;

function StatusAnnouncer({ className, ...props }: StatusAnnouncerProps = {}) {
  return (
    <StatusAnnouncerPrimitive
      className={(state) => `media-status-announcer ${resolveClassName(className, state)}`}
      {...props}
    />
  );
}

type VolumeIndicatorProps = Omit<VolumeIndicatorPrimitive.RootProps, 'children'>;

function VolumeIndicator({ className, ...props }: VolumeIndicatorProps = {}) {
  return (
    <VolumeIndicatorPrimitive.Root
      className={(state) => `media-volume-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <VolumeIndicatorPrimitive.Fill className={'media-volume-indicator-fill'}>
        <>
          <span className="contents theme-minimal:hidden">
            <VolumeHighIconPrimitive className={'media-volume-indicator-high-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <VolumeHighIconPrimitive2 className={'media-volume-indicator-high-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <VolumeLowIconPrimitive className={'media-volume-indicator-low-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <VolumeLowIconPrimitive2 className={'media-volume-indicator-low-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <VolumeOffIconPrimitive className={'media-volume-indicator-off-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <VolumeOffIconPrimitive2 className={'media-volume-indicator-off-icon'} />
          </span>
        </>
        <VolumeIndicatorPrimitive.Value className={'media-volume-indicator-value'} />
      </VolumeIndicatorPrimitive.Fill>
    </VolumeIndicatorPrimitive.Root>
  );
}

type StatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

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

function StatusIndicator({ className, ...props }: StatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={TOP_STATUS_ACTIONS}
      className={(state) => `media-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <div className={'media-status-indicator-content'}>
        <>
          <span className="contents theme-minimal:hidden">
            <CaptionsOnIconPrimitive className={'media-status-indicator-captions-on-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CaptionsOnIconPrimitive2 className={'media-status-indicator-captions-on-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <CaptionsOffIconPrimitive className={'media-status-indicator-captions-off-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <CaptionsOffIconPrimitive2 className={'media-status-indicator-captions-off-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <FullscreenEnterIconPrimitive className={'media-status-indicator-fullscreen-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <FullscreenEnterIconPrimitive2 className={'media-status-indicator-fullscreen-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <FullscreenExitIconPrimitive className={'media-status-indicator-fullscreen-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <FullscreenExitIconPrimitive2 className={'media-status-indicator-fullscreen-exit-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <PipEnterIconPrimitive className={'media-status-indicator-pip-enter-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PipEnterIconPrimitive2 className={'media-status-indicator-pip-enter-icon'} />
          </span>
        </>
        <>
          <span className="contents theme-minimal:hidden">
            <PipExitIconPrimitive className={'media-status-indicator-pip-exit-icon'} />
          </span>
          <span className="hidden theme-minimal:contents">
            <PipExitIconPrimitive2 className={'media-status-indicator-pip-exit-icon'} />
          </span>
        </>
        <StatusIndicatorPrimitive.Value className={'media-status-indicator-value'} />
      </div>
    </StatusIndicatorPrimitive.Root>
  );
}

type SeekIndicatorProps = Omit<SeekIndicatorPrimitive.RootProps, 'children'>;

function SeekIndicator({ className, ...props }: SeekIndicatorProps = {}) {
  return (
    <SeekIndicatorPrimitive.Root
      className={(state) => `media-seek-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <>
        <span className="contents theme-minimal:hidden">
          <ChevronIconPrimitive className={'media-seek-indicator-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <ChevronIconPrimitive2 className={'media-seek-indicator-icon'} />
        </span>
      </>
      <SeekIndicatorPrimitive.Value className={'media-seek-indicator-value'} />
    </SeekIndicatorPrimitive.Root>
  );
}

type PlaybackStatusIndicatorProps = Omit<StatusIndicatorPrimitive.RootProps, 'children'>;

const PLAYBACK_STATUS_ACTIONS = ['togglePaused'] as const;

function PlaybackStatusIndicator({ className, ...props }: PlaybackStatusIndicatorProps = {}) {
  return (
    <StatusIndicatorPrimitive.Root
      actions={PLAYBACK_STATUS_ACTIONS}
      className={(state) => `media-playback-status-indicator ${resolveClassName(className, state)}`}
      {...props}
    >
      <>
        <span className="contents theme-minimal:hidden">
          <PlayIconPrimitive className={'media-playback-status-indicator-play-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <PlayIconPrimitive2 className={'media-playback-status-indicator-play-icon'} />
        </span>
      </>
      <>
        <span className="contents theme-minimal:hidden">
          <PauseIconPrimitive className={'media-playback-status-indicator-pause-icon'} />
        </span>
        <span className="hidden theme-minimal:contents">
          <PauseIconPrimitive2 className={'media-playback-status-indicator-pause-icon'} />
        </span>
      </>
    </StatusIndicatorPrimitive.Root>
  );
}

function VideoStatusIndicators({ className, ...props }: VideoStatusIndicatorsProps = {}) {
  return (
    <>
      <StatusAnnouncer />
      <div className={`media-video-status-indicators ${className ?? ''}`} {...props}>
        <VolumeIndicator />
        <StatusIndicator />
        <SeekIndicator />
        <PlaybackStatusIndicator />
      </div>
    </>
  );
}

function DefaultVideoSkin({ children, className, poster, ...props }: DefaultVideoSkinProps = {}) {
  const isPosterString = typeof poster === 'string';

  return (
    <Container className={`media-skin media-skin--video ${className ?? ''}`} {...props}>
      {children}
      <Poster src={isPosterString ? poster : undefined}>{isPosterString ? undefined : poster}</Poster>
      <BufferingIndicator />
      <ErrorDialog />

      <DefaultVideoControls />

      <VideoHotkeys />
      <VideoGestures />
      <VideoStatusIndicators />
    </Container>
  );
}

const Skin = DefaultVideoSkin;

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

export interface VideoPlayerProps {
  children?: ReactNode;
  style?: CSSProperties;
  className?: string;
  renderPoster?: Poster.Props['render'];
}

export function VideoSkin({ renderPoster, ...props }: VideoSkinProps) {
  return <Skin poster={renderPoster} {...props} />;
}

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

function Button({ className, ...props }: ButtonProps) {
  return <button className={`media-button ${className ?? ''}`} {...props} />;
}

type SliderTrackProps = ComponentProps<'div'>;

function SliderTrack({ className, ...props }: SliderTrackProps) {
  return <div className={`media-slider-track ${className ?? ''}`} {...props} />;
}

type SliderFillProps = ComponentProps<'div'>;

function SliderFill({ className, ...props }: SliderFillProps) {
  return <div className={`media-slider-fill ${className ?? ''}`} {...props} />;
}

type SliderThumbProps = ComponentProps<'div'>;

function SliderThumb({ className, ...props }: SliderThumbProps) {
  return <div className={`media-slider-thumb ${className ?? ''}`} {...props} />;
}

function VolumePopover({
  className,
  popupClassName,
  showTooltip = false,
  side = 'top',
  orientation = 'vertical',
  ...props
}: VolumePopoverProps = {}) {
  return (
    <VolumePopoverPrimitive.Root openOnHover delay={200} closeDelay={100} side={side} {...props}>
      <ButtonTooltip delay={0} disabled={!showTooltip} sticky side="top">
        <VolumePopoverPrimitive.Trigger render={<MuteButton className={className} />} />
      </ButtonTooltip>
      <VolumePopoverPrimitive.Popup className={`media-volume-popover ${popupClassName}`}>
        <VolumeSlider orientation={orientation} />
      </VolumePopoverPrimitive.Popup>
    </VolumePopoverPrimitive.Root>
  );
}

type SliderBufferProps = ComponentProps<'div'>;

function SliderBuffer({ className, ...props }: SliderBufferProps) {
  return <div className={`media-slider-buffer ${className ?? ''}`} {...props} />;
}

// ================================================================
// Error Dialog
// ================================================================

function ErrorDialog({ className, ...props }: ErrorDialogProps = {}) {
  return (
    <ErrorDialogPrimitive.Root>
      <ErrorDialogPrimitive.Backdrop className={'media-dialog-backdrop'} />
      <ErrorDialogPrimitive.Popup
        className={(state) => `media-dialog-popup ${resolveClassName(className, state)}`}
        {...props}
      >
        <div className={'media-dialog-content'}>
          <ErrorDialogPrimitive.Title className={'media-dialog-title'} />
          <ErrorDialogPrimitive.Description className={'media-dialog-description'} />
        </div>
        <div className={'media-dialog-actions'}>
          <ErrorDialogPrimitive.Close render={<Button />} className={'media-dialog-close'} />
        </div>
      </ErrorDialogPrimitive.Popup>
    </ErrorDialogPrimitive.Root>
  );
}

Get
Started

Everything you need to install and launch your player

v10 is built different

Video.js 10 is complete ground-up rewrite of the player for the modern web. The UI is separated from the underlying media renderer. Every component is independent and works together through open API contracts. We've structured the project for modern JavaScript bundlers to support tree-shaking and intelligent code splitting.

Start with a small player and only add what you need.

Download speeds

(based on slow 4G connection)

VJS 8

3.96s/200kB

VJS 10

0.75s/38kB

0s
1s
2s
3s
4s
Streaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sitesStreaming bits on millions of sites

Roadmap

Oct 2025

Tech Preview

Kick the tires, and light the fires

Mar 2026

Beta

Close to stable. Experimental adoption in real projects

Mid 2026

GA

Stable APIs. Feature parity w/ Media Chrome, Vidstack, Plyr.

End of 2026

Core Feature Parity

Video.js core/contrib plugins migrated

Sponsors

Corporate Shepherd

The role of Corporate Shepherd is held by the company that has been elected by the Video.js Technical Steering Committee (TSC) to be a steward of the project and make significant investment into the development of Video.js. The role is currently held by Mux, taking over for Brightcove in 2025. Mux is a leader in streaming video technology and was founded by Steve Heffernan, the creator of Video.js.

CDN

Emeritus Sponsor

Device Testing

Static Hosting