Skip to main content
The VideoPlayer component provides a full-featured media player with video/audio mode switching, playlist support, and keyboard controls.

VideoPlayer

The main video player component with playback controls and context.

Usage

import { VideoPlayer } from "@/components/video-player/VideoPlayer";
import { VideoPlayerOverlay } from "@/components/video-player/VideoPlayerOverlay";
import type { WatchListItem } from "@/context/playerContext";

interface Props {
  watchListItem: WatchListItem;
  background: boolean;
}

export function VideoPage({ watchListItem, background }: Props) {
  return (
    <VideoPlayer
      watchListItem={watchListItem}
      background={background}
      overlay={<VideoPlayerOverlay {...props} />}
    >
      {/* Sidebar content */}
    </VideoPlayer>
  );
}

Props

watchListItem
WatchListItem
required
Contains video, entry, and theme data:
interface WatchListItem {
  watchListId: string;
  video: VideoSummaryCardVideoFragment;
  entry: VideoSummaryCardEntryFragment;
}
background
boolean
required
Whether player is in background/mini mode
children
ReactNode
Sidebar content displayed next to video
overlay
ReactNode
Overlay content (typically VideoPlayerOverlay)

VideoPlayerContext

The component provides a context with player controls:
interface VideoPlayerContextValue {
  video: VideoSummaryCardVideoFragment;
  entry: VideoSummaryCardEntryFragment;
  background: boolean;
  videoPagePath: string;
  playerRef: RefObject<HTMLVideoElement | HTMLAudioElement | null>;
  progressRef: RefObject<HTMLDivElement | null>;
  bufferedRef: RefObject<HTMLDivElement | null>;
  previousVideoPath: string | null;
  playPreviousTrack(navigate: boolean): void;
  nextVideoPath: string | null;
  playNextTrack(navigate: boolean): void;
  isPlaying: boolean;
  togglePlay(): void;
  videoUrl: string;
  audioUrl: string;
  updateAudioMode(audioMode: string): void;
  togglePip(): void;
}

Accessing Context

From /src/components/video-player/VideoPlayerBar.tsx:90:
import { VideoPlayerContext } from "@/components/video-player/VideoPlayer";

export function VideoPlayerBar() {
  const context = useContext(VideoPlayerContext);
  
  if (!context) {
    throw new Error("VideoPlayerBar needs to be inside VideoPlayer!");
  }
  
  const { 
    isPlaying, 
    togglePlay, 
    playNextTrack, 
    playPreviousTrack 
  } = context;
  
  // Use context values
}

Features

  • Dual Mode: Switches between video and audio-only mode
  • Playlist Support: Navigate between tracks in watch list
  • Auto-play: Configurable auto-play for next track
  • Keyboard Shortcuts: Full keyboard control
  • Picture-in-Picture: PiP support for video mode
  • Frame-accurate Seeking: Step through individual frames
  • Media Session API: System media controls integration

Keyboard Shortcuts

From /src/components/video-player/VideoPlayer.tsx:323:
Space / K
shortcut
Play / Pause
shortcut
Seek forward 5 seconds
shortcut
Seek backward 5 seconds
L
shortcut
Seek forward 10 seconds
J
shortcut
Seek backward 10 seconds
.
shortcut
Seek forward 1 frame (when paused)
,
shortcut
Seek backward 1 frame (when paused)
N
shortcut
Next track
B
shortcut
Previous track
M
shortcut
Mute / Unmute
shortcut
Volume up
shortcut
Volume down
F
shortcut
Toggle fullscreen
A
shortcut
Toggle audio mode
P
shortcut
Toggle picture-in-picture
D
shortcut
Download current track

Source

Defined in /src/components/video-player/VideoPlayer.tsx:58

VideoPlayerBar

The control bar with playback controls and track information.

Usage

import { VideoPlayerBar } from "@/components/video-player/VideoPlayerBar";

// Automatically included in VideoPlayer
<VideoPlayer {...props}>
  {/* VideoPlayerBar is rendered internally */}
</VideoPlayer>

Features

  • Track Information: Song title, artist, anime name
  • Playback Controls: Previous, play/pause, next buttons
  • Volume Control: Slider and mute button
  • Progress Bar: Seek through video
  • Actions: Add to playlist, share, close player

Layout

From /src/components/video-player/VideoPlayerBar.tsx:23:
const StyledPlayerBar = styled(Solid)`
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-gap: 16px;
  align-items: center;
  
  // Responsive: 2 columns on tablet
  @media (max-width: ${theme.breakpoints.tabletMax}) {
    grid-template-columns: 1fr auto;
  }
`;

Source

Defined in /src/components/video-player/VideoPlayerBar.tsx:89

VideoPlayerOverlay

Overlay controls for settings and video options.

Usage

import { VideoPlayerOverlay } from "@/components/video-player/VideoPlayerOverlay";
import type { VideoPageProps } from "@/pages/anime/[animeSlug]/[videoSlug]";

export function VideoPage(props: VideoPageProps) {
  return (
    <VideoPlayer
      overlay={<VideoPlayerOverlay {...props} />}
      {...otherProps}
    />
  );
}

Props

anime
AnimeFragment
required
Anime data
themeIndex
number
required
Index of current theme
entryIndex
number
required
Index of current entry
videoIndex
number
required
Index of current video

Features

  • Add to Playlist: Quick playlist addition
  • Keyboard Shortcuts Dialog: Shows all keyboard shortcuts
  • Share Menu: Share video or audio
  • Settings Menu: Switch between video/audio mode
  • Version Selector: Choose different video versions
  • Picture-in-Picture: Toggle PiP mode
  • Fullscreen: Toggle fullscreen mode

Example from Overlay

From /src/components/video-player/VideoPlayerOverlay.tsx:124:
<StyledOverlay>
  <Row style={{ "--gap": "16px" }}>
    <PlaylistTrackAddDialog
      video={video}
      entry={{ ...entry, theme }}
      trigger={<StyledOverlayButton icon={faPlus} isCircle />}
    />
    
    <Dialog>
      <DialogTrigger asChild>
        <StyledOverlayButton icon={faKeyboard} isCircle />
      </DialogTrigger>
      <DialogContent title="Keyboard Shortcuts">
        {/* Shortcuts list */}
      </DialogContent>
    </Dialog>
    
    <ShareMenu {...props} />
    <Menu>{/* Settings */}</Menu>
    {isPIPSupported && <PiPButton />}
    <FullscreenButton />
  </Row>
</StyledOverlay>

Source

Defined in /src/components/video-player/VideoPlayerOverlay.tsx:90

VolumeControl

Volume slider with mute button.

Usage

import { VolumeControl } from "@/components/video-player/VolumeControl";

<VolumeControl />

Features

  • Dynamic Icon: Changes based on volume level (high, low, off, muted)
  • Hover Slider: Slider appears on hover
  • Mute Toggle: Click icon to mute/unmute
  • Global State: Syncs volume across all players using settings

Implementation

From /src/components/video-player/VolumeControl.tsx:29:
export function VolumeControl(props: ComponentPropsWithoutRef<typeof StyledRow>) {
  const [volume, setVolume] = useSetting(GlobalVolume);
  const [muted, setMuted] = useSetting(Muted);

  let icon;
  if (muted) {
    icon = faVolumeXmark;
  } else if (volume > 0.5) {
    icon = faVolumeHigh;
  } else if (volume > 0) {
    icon = faVolumeLow;
  } else {
    icon = faVolumeOff;
  }

  return (
    <StyledRow style={{ "--gap": "8px" }} {...props}>
      <IconTextButton 
        icon={icon} 
        isCircle 
        onClick={() => setMuted(!muted)} 
      />
      <StyledSlider
        value={[volume]}
        onValueChange={([volume]) => { 
          setVolume(volume); 
          setMuted(false); 
        }}
        min={0}
        max={1}
        step={0.01}
      />
    </StyledRow>
  );
}

Source

Defined in /src/components/video-player/VolumeControl.tsx:29

ProgressBar

Seekable progress bar for video playback (component available in /src/components/video-player/ProgressBar.tsx).

Complete Example

Full video player implementation:
import { VideoPlayer } from "@/components/video-player/VideoPlayer";
import { VideoPlayerOverlay } from "@/components/video-player/VideoPlayerOverlay";
import { Column } from "@/components/box/Flex";
import { ThemeDetailCard } from "@/components/card/ThemeDetailCard";

export default function VideoPage(props: VideoPageProps) {
  const { anime, themeIndex, entryIndex, videoIndex } = props;
  const theme = anime.themes[themeIndex];
  const entry = theme.entries[entryIndex];
  const video = entry.videos[videoIndex];

  const watchListItem = {
    watchListId: video.basename,
    video,
    entry: { ...entry, theme }
  };

  return (
    <VideoPlayer
      watchListItem={watchListItem}
      background={false}
      overlay={<VideoPlayerOverlay {...props} />}
    >
      <Column style={{ "--gap": "24px" }}>
        {anime.themes.map((theme) => (
          <ThemeDetailCard key={theme.slug} theme={theme} />
        ))}
      </Column>
    </VideoPlayer>
  );
}

Build docs developers (and LLMs) love