Video Player

A native way to play media: a real video element wrapped in a composable control surface. The scrubber and volume bar are Radix Sliders, and the controls auto-hide and stay legible over any frame.

0:00 / 0:00

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add video-player

Usage#

usage.tsxTSX
import {  VideoPlayer,  Video,  VideoControls,  VideoBar,  VideoPlayButton,  VideoSeek,  VideoTime,  VideoVolume,  VideoFullscreen,  VideoSpinner,} from "@/components/ui/video-player"export function Example() {  return (    <VideoPlayer>      <Video src="/clip.mp4" poster="/clip.jpg" preload="metadata" />      <VideoSpinner />      <VideoControls>        <VideoBar>          <VideoPlayButton />          <VideoSeek />          <VideoTime />          <VideoVolume />          <VideoFullscreen />        </VideoBar>      </VideoControls>    </VideoPlayer>  )}

Anatomy#

The player is composed from named parts, never dot-notation. Drop in only the controls you need and arrange them however you like; every part reads the shared playback state from VideoPlayer's context, so order and grouping are entirely up to you.

anatomy.tsxTSX
<VideoPlayer>  <Video src={src} poster={poster} preload="metadata" />  <VideoSpinner />  <VideoControls>    <VideoBar>      <VideoPlayButton />      <VideoSeek />      <VideoTime />      <VideoVolume />      <VideoFullscreen />    </VideoBar>  </VideoControls></VideoPlayer>

Minimal#

Nothing is required but VideoPlayer and Video. Here's a stripped-back bar with just play, scrubber, and timecode on a single row.

0:00 / 0:00

Scrub preview#

Hover (or drag) the scrubber and a timecode bubble rides the cursor, showing the moment you'd land on before you commit, so you can see where you're heading and, against the total in VideoTime, how much is left. It's built into VideoSeek, with no extra props. With preload="metadata" the duration is known up front, so it works before you ever press play.

0:00 / 0:00

Hover controls#

By default the controls stay up while the video is paused and auto-hide after an idle beat during playback. Pass revealOn="hover" for the cinematic treatment instead: the chrome is hidden until you point at the player, paused or playing alike, and reveals again on hover or when you tab into it (keyboard users are never stranded). The idle timer is skipped in this mode, visibility simply tracks the pointer.

It pairs naturally with the native <video> props that Video forwards: autoPlay, loop, and muted turn it into an ambient product loop with mute and zoom (fullscreen) one hover away. Browsers only allow autoplay on a muted video, so keep muted on whenever you set autoPlay.

0:00 / 0:00

Labels#

Every word the player speaks, from the play tooltip to the slider names a screen reader announces, comes from labels. Pass the keys you want to change; the rest keep their English defaults.

0:00 / 0:00

Controlled volume#

Pass volume and muted to own the level. The volume bar, the mute button and the arrow keys then show your values and report changes through onVolumeChange and onMutedChange, and the player stops writing volume or mute to the <video>. That is what lets you remember the level between visits, or play the sound somewhere else: audio tracks recorded apart from the picture and kept in sync with a video that stays muted. The demo keeps the level in state and applies it to the element itself.

0:00 / 0:00

Saved level: 40%

Reaching the element#

A ref on Video reaches the real <video>; the player keeps its own ref alongside yours. Media handlers you pass (onPlay, onTimeUpdate, onVolumeChange and the rest) run after the player's own, so listening to the element never cuts the controls off from its state.

element.tsxTSX
const videoRef = React.useRef<HTMLVideoElement>(null)// Slave separately recorded audio to the video clock, recover a stalled decoder...React.useEffect(() => attachAudioTracks(videoRef.current), [])<VideoPlayer>  <Video    ref={videoRef}    src={src}    // Runs after the player's own handler, so the controls keep up.    onTimeUpdate={(e) => saveProgress(e.currentTarget.currentTime)}  />  …</VideoPlayer>

Keyboard#

Click the video to focus the player, then drive it from the keyboard. The Radix scrubber and volume slider keep their own arrow-key handling when focused, so the shortcuts below only fire when the player frame itself holds focus.

KeyAction
Space / KPlay / pause
← / →Seek 5 seconds
↑ / ↓Volume up / down
MMute / unmute
FToggle fullscreen

FAQ#

Only `VideoPlayer` and `Video`. Everything else, including `VideoControls`, `VideoBar`, and the individual controls, is optional. Every part reads playback state from the `VideoPlayer` context, so you drop in just the controls you need and arrange them in any order.