Media

Custom Video Player

A video player with a custom control bar that fades out automatically during playback, supporting scrub-to-seek, volume, fullscreen, and keyboard operation.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/custom-video-player.en.json?t=<install token>"

New project or installation trouble? Start here →

Custom Video Player is a React video player component that wraps the HTML5 <video> element with Tailwind CSS and Motion. It replaces the browser's default controls with a custom control bar that has scrub-to-seek, a buffered indicator, volume, fullscreen, and a full set of keyboard shortcuts.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

2400
Active edited usageDefault props
<CustomVideoPlayer/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Demo videos on a product homepage or feature page, where the controls should match the rest of the site.
  • Screen recordings embedded in online courses or docs, where viewers scrub and skip with the keyboard a lot.
  • Playing user-uploaded video inside an admin panel or SaaS app, including MediaRecorder blobs whose duration is Infinity.
  • Muted, looping background video on a landing page, using autoPlay loop muted.

Installation

npx webberui@latest add "https://webberui.com/r/custom-video-player.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/custom-video-player.

Usage

import { CustomVideoPlayer } from "@/components/ui/custom-video-player";

<CustomVideoPlayer
  src="/videos/demo.mp4"
  poster="/videos/demo-poster.jpg"
  className="w-full max-w-2xl"
/>

Most browsers require muted alongside autoplay:

<CustomVideoPlayer src="/videos/loop.mp4" autoPlay loop muted />

Examples

Sync playback with outside state

Use onPlayStateChange to pause other animations on the page while the video plays, or to send a view event.

"use client";

import * as React from "react";
import { CustomVideoPlayer } from "@/components/ui/custom-video-player";

export function ProductDemo() {
  const [playing, setPlaying] = React.useState(false);

  return (
    <div className="space-y-2">
      <CustomVideoPlayer
        src="/videos/product-tour.mp4"
        poster="/videos/product-tour.jpg"
        label="Product tour video"
        onPlayStateChange={setPlaying}
      />
      <p className="text-sm text-muted-foreground">
        {playing ? "Playing" : "Paused"}
      </p>
    </div>
  );
}

Vertical video with always-visible controls

Set aspectRatio to "9 / 16" and pass autoHide={false} so the control bar never fades out.

<CustomVideoPlayer
  src="/videos/reel.mp4"
  aspectRatio="9 / 16"
  autoHide={false}
  className="max-w-xs"
/>

Play back a recorded blob

src accepts a URL from URL.createObjectURL; the component corrects the Infinity duration these blobs often report.

"use client";

import * as React from "react";
import { CustomVideoPlayer } from "@/components/ui/custom-video-player";

export function RecordingPreview({ blob }: { blob: Blob }) {
  const [url, setUrl] = React.useState<string>();

  React.useEffect(() => {
    const next = URL.createObjectURL(blob);
    setUrl(next);
    return () => URL.revokeObjectURL(next);
  }, [blob]);

  if (!url) return null;
  return <CustomVideoPlayer src={url} label="Recording preview" hideControlsDelay={1500} />;
}

Props

PropTypeDefaultDescription
srcstring—Video source URL
posterstring—Cover image, shown before playback
autoPlaybooleanfalseAutoplay (usually needs muted alongside it)
loopbooleanfalseLoop playback
mutedbooleanfalseWhether it starts muted
hideControlsDelaynumber2400How many milliseconds of inactivity during playback before the control bar fades out
autoHidebooleantrueWhether the control bar fades out automatically
aspectRatiostring"16 / 9"The container's CSS aspect-ratio
labelstring"Video Player" (Video player)Accessible label for the player container
onPlayStateChange(playing: boolean) => void—Callback when the playback state changes
classNamestring—Appended to the container's className

How it works

  • The control bar fades out and hides the cursor while playing with no interaction, and reappears immediately on cursor movement, touch, focus, or a key press; it stays lit while paused or once playback ends.
  • The progress bar can be clicked to seek and dragged to scrub (using pointer capture, so it keeps working even if the cursor leaves the player).
  • Duration correction is built in: for streams or MediaRecorder blobs whose duration is Infinity, a recalculation is triggered automatically so the progress and the time display stay correct.
  • The volume slider is collapsed normally and expands when the mouse enters or focus lands on the volume area.

Accessibility

  • When the user has "reduce motion" enabled at the system level, the control bar's automatic fade-out is disabled and it stays lit, with transition durations set to zero.
  • Complete keyboard operation (once the player has focus):
    • Space / K: play/pause
    • ← / → (or J / L): back/forward 5 seconds
    • ↑ / ↓: volume up/down
    • Home / End: jump to the start/end
    • M: toggle mute F: toggle fullscreen
  • The progress bar is a role="slider" with aria-valuemin/max/now and aria-valuetext; every control button has an aria-label, and the mute and fullscreen buttons reflect their state with aria-pressed.

FAQ

Does it work with the Next.js App Router?

Yes. The source already starts with "use client", so import it from a Server Component page as usual; no extra wrapper is needed.

Why doesn't autoplay work?

Most browsers only allow muted video to autoplay, so pass muted together with autoPlay. The component also sets playsInline, so iOS Safari does not force fullscreen.

How do I change the fade-out delay or turn it off?

hideControlsDelay sets how many milliseconds of inactivity during playback pass before the control bar fades (default 2400), and autoHide={false} keeps it visible. The control bar also stays visible when the user has reduced motion enabled.

Does it download the whole video up front?

No. The <video> element uses preload="metadata", so only metadata such as duration loads at first; buffering starts once playback begins.

What are the dependencies?

motion, lucide-react, clsx, and tailwind-merge, plus cn from @/lib/utils. If you don't use the CLI, copy the source from the Manual tab.