Portfolio

Video Player

A composable, accessible custom video player with auto-hiding controls, keyboard shortcuts, picture-in-picture, fullscreen, and icon-library-agnostic icons.

Installation

pnpm dlx shadcn@latest add https://ui.ahmet.studio/r/video-player

Usage

import { VideoPlayer } from "@/components/ui/video-player"; export default function Example() { return ( <VideoPlayer src="/path/to/video.mp4" poster="/path/to/poster.jpg" className="aspect-video" /> ); }

Features

  • Auto-hiding controls — controls fade out while playing and reappear on mouse movement or focus.
  • Seek bar with buffered indicator — Radix slider for smooth scrubbing with a behind-it buffered range track.
  • Volume with mute toggle — hover the volume button to reveal an inline slider.
  • Playback speed menu — 0.5x through 2x via a dropdown.
  • Picture-in-Picture — toggle button only renders when supported by the browser.
  • Fullscreen — uses the standard fullscreen API on the player container.
  • Loading spinner — shows while the video is buffering.
  • Imperative handle — pass a ref to call .play(), .pause(), .seek(s), or grab the underlying <video>.

Keyboard Shortcuts

KeyAction
Space / kPlay / pause
/ Seek ±5 seconds
/ Volume up / down (5%)
mToggle mute
fToggle fullscreen

Props

PropTypeDefaultDescription
srcstringrequiredVideo source URL.
posterstringPoster image shown before playback.
classNamestringApplied to the <video> element.
containerClassNamestringApplied to the outer container.
autoHideControlsMsnumber2500Idle timeout before controls auto-hide while playing. Set 0 to keep them visible.

All other native <video> attributes (autoPlay, loop, muted, event handlers, etc.) are forwarded to the underlying element.

Imperative API

import { useRef } from "react"; import { VideoPlayer, type VideoPlayerHandle, } from "@/components/ui/video-player"; export function Controlled() { const ref = useRef<VideoPlayerHandle>(null); return ( <div className="flex flex-col gap-2"> <VideoPlayer ref={ref} src="/video.mp4" className="aspect-video" /> <div className="flex gap-2"> <button onClick={() => ref.current?.play()}>Play</button> <button onClick={() => ref.current?.pause()}>Pause</button> <button onClick={() => ref.current?.seek(30)}>Skip to 0:30</button> </div> </div> ); }

Accessibility

  • The container is keyboard-focusable; all shortcuts dispatch from there.
  • Every control button has an aria-label and a tooltip.
  • The Picture-in-Picture button is hidden when the browser does not support it.
  • Focus rings honour the global ring token.

Icon Library Swap

The component is authored with <IconPlaceholder lucide="..." tabler="..." hugeicons="..." phosphor="..." />. When you install with npx shadcn@latest add, the shadcn CLI looks at your components.json iconLibrary setting and rewrites every placeholder to the chosen library's native component, replacing the import accordingly. No runtime cost is added on the consumer side.

Set your icon library in components.json:

{ "iconLibrary": "tabler" }

Then install — the imports and JSX will use @tabler/icons-react automatically.