HomeProjectsComponentsNotes
Theme

Video Player

v0.1.1

A headless, composable React video/audio player

GitHubnpm

On this page

  • Introduction
  • Installation
  • Quick Start
  • Usage
  • Props
  • Configuration
  • Examples
  • Troubleshooting

Introduction

Video Player (published as mediakit-player) is a headless, composable React video/audio player. Instead of a single monolithic <VideoPlayer />, it ships a <Media.Provider> context and a set of small, pre-styled building-block components — play button, seek bar, volume, captions toggle, playback rate, picture-in-picture, and fullscreen — that you compose into your own control bar. They look right the moment you import them, and layout-level pieces (Video, Audio, Media.View, Media.Controls, Media.ControlBar, Media.Group, Media.TimeGroup) take className if you want to restyle.

Everything under <Media.Provider> shares playback state through React context, so you can rearrange, omit, or restyle any control without touching the others.

Highlights

  • Looks right out of the box — components render fully styled the moment you import them, no CSS import or Tailwind setup required in your app
  • Composable primitives instead of a fixed layout
  • Full playback control: play/pause, seek, volume/mute, playback rate, duration/remaining-time toggle
  • Picture-in-picture and fullscreen built in
  • Double-click/tap to seek ±10s with an animated forward/backward indicator
  • Hover scrubbing preview on the seek bar, with generated video thumbnails
  • Video and audio through the same <Media.Provider> context
  • Fully typed — ships its own .d.ts files, no @types package needed
  • Tree-shakeable ESM + CJS builds via tsup — only the bundled stylesheet is marked as a side effect

Installation

Install the package with your package manager of choice:

npm install mediakit-player

mediakit-player expects react and react-dom (>=17) as peer dependencies, provided by your app:

npm install react react-dom

Quick Start

Wrap your player in <Media.Provider>, mount a <Video> (or <Audio>) element inside <Media.View>, and compose a control bar from the primitives you need. The components render fully styled the moment you import them — no CSS import or Tailwind setup required in your app:

"use client";
 
import { Media, Video } from "mediakit-player";
 
export default function Player() {
  return (
    <Media.Provider>
      <Media.View className="media-view">
        <Video src="/videos/sample.mp4" />
 
        <Media.Controls>
          <Media.ControlBar>
            <Media.Group>
              <Media.PlayButton />
              <Media.Volume />
            </Media.Group>
 
            <Media.TimeGroup>
              <Media.Time />
              <Media.SeekBar />
              <Media.Duration />
            </Media.TimeGroup>
 
            <Media.Group>
              <Media.Captions />
              <Media.PlaybackRate />
              <Media.PictureInPicture />
              <Media.Fullscreen />
            </Media.Group>
          </Media.ControlBar>
        </Media.Controls>
      </Media.View>
    </Media.Provider>
  );
}

Usage

Audio

Swap <Video> for <Audio> — same provider, same controls:

import { Media, Audio } from "mediakit-player";
 
<Media.Provider>
  <Media.View>
    <Audio src="/audio/track.mp3" />
    <Media.Controls>
      <Media.ControlBar>
        <Media.PlayButton />
        <Media.SeekBar />
        <Media.Volume />
      </Media.ControlBar>
    </Media.Controls>
  </Media.View>
</Media.Provider>;

Custom controls with useMedia()

For fully custom UI, drop down to the hook. It must be called from a component rendered under <Media.Provider>:

import { useMedia } from "mediakit-player";
 
function CustomButton() {
  const { isPlaying, togglePlay, currentTime, duration } = useMedia();
 
  return (
    <button onClick={togglePlay}>
      {isPlaying ? "Pause" : "Play"} — {Math.round(currentTime)}s /{" "}
      {Math.round(duration)}s
    </button>
  );
}

Interaction behavior worth knowing

  • Single-clicking the video toggles play/pause (debounced against double-click).
  • Double-clicking the left or right half of the video seeks ±10s and flashes a forward/backward indicator.
  • ArrowLeft / ArrowRight seek ±10s globally once a media element is attached.
  • Media.Fullscreen toggles fullscreen on the <Media.View> container, not the raw <video> element.

Props

Video, Audio, Media.View, Media.Controls, Media.ControlBar, Media.Group, and Media.TimeGroup accept className and forward remaining DOM props, so Tailwind, CSS Modules, or plain CSS all work for layout. Media.Title accepts only children. The individual controls (PlayButton, SeekBar, Time, Duration, Volume, Captions, PlaybackRate, PictureInPicture, Fullscreen) don't take className or props yet — restyling them currently means overriding their built-in classes globally.

Video / Audio

Prop Type Default Description
src string — Required media source URL.
className string "" Merged with the component's base classes.
...props VideoHTMLAttributes / AudioHTMLAttributes — Any other native <video> / <audio> attribute.

Media.View

Prop Type Description
className string Extends the default aspect-ratio/rounded container styling.
...props HTMLAttributes<HTMLDivElement> Forwarded to the wrapping <div> — this is also the fullscreen target.

useMedia() return value

Field Type Description
mediaElement HTMLVideoElement | HTMLAudioElement | null The attached media element.
isPlaying boolean Whether playback is active.
currentTime / duration number Playback position and total duration, in seconds.
volume / isMuted number / boolean Current volume (0–1) and mute state.
playbackRate number Current playback speed.
showRemainingTime boolean Whether Media.Duration is showing remaining time.
togglePlay() () => void Toggles play/pause.
seekTo(time) / seekBy(delta) (number) => void Absolute or relative seeking, clamped to [0, duration].
setVolume(value) / toggleMute() (number) => void / () => void Volume control, clamped to [0, 1].
setPlaybackRate(rate) (number) => void Sets playback speed.
toggleFullscreen() () => void Toggles fullscreen on the Media.View container.
togglePictureInPicture() () => Promise<void> Toggles native picture-in-picture (video only).
toggleShowRemainingTime() () => void Toggles the elapsed/remaining time display.

Configuration

  • Peer dependencies — react and react-dom >=17 must already be present in your app; they are not bundled.
  • Module formats — ships both ESM (dist/index.js) and CJS (dist/index.cjs) builds via tsup, with bundled .d.ts types, so no @types/mediakit-player package is needed.
  • Tree-shaking — unused components are still dropped by bundlers that support tree-shaking; only the bundled stylesheet (dist/index.css) is marked as a side effect so it survives.
  • Styling — the package's entry point has a built-in import "./index.css", so any bundler-based app (Next.js, Vite, Create React App, etc.) picks up the compiled Tailwind stylesheet automatically and extracts a real <link> at build time — nothing to import yourself, and components look right immediately. Override the look with className on the components that accept it (see Props), or globally for the individual controls. If you're in an environment without a bundler processing CSS from node_modules (a bare Node script, or Jest without a CSS transform configured), either add a CSS mock for your test runner or import the stylesheet explicitly with import "mediakit-player/style.css".
  • Server Components — every interactive piece (Video, Audio, the controls, useMedia()) is marked "use client" internally; you still need "use client" at the top of your own file if you're composing them directly in a Next.js App Router page.

Examples

Minimal control bar

<Media.Provider>
  <Media.View>
    <Video src="/videos/clip.mp4" />
    <Media.Controls>
      <Media.ControlBar>
        <Media.PlayButton />
        <Media.SeekBar />
      </Media.ControlBar>
    </Media.Controls>
  </Media.View>
</Media.Provider>

Reading playback progress elsewhere on the page

function ProgressLabel() {
  const { currentTime, duration } = useMedia();
  const pct = duration ? Math.round((currentTime / duration) * 100) : 0;
  return <span>{pct}% watched</span>;
}
 
<Media.Provider>
  <Media.View>
    <Video src="/videos/clip.mp4" />
  </Media.View>
  <ProgressLabel />
</Media.Provider>;

ProgressLabel works anywhere inside <Media.Provider> — it doesn't need to be a sibling of <Media.View>.

Troubleshooting

"Media components must be used within <Media.Provider>" useMedia() throws this if it's called from a component that isn't rendered under <Media.Provider>. Make sure every custom control and every built-in Media.* component is nested inside the provider.

Picture-in-picture silently does nothing togglePictureInPicture() no-ops with a console warning when the browser doesn't support the Picture-in-Picture API, when it's called on an <Audio> element, or when disablePictureInPicture is set on the <video>.

Fullscreen doesn't cover the whole player Media.Fullscreen targets the <Media.View> container (via containerRef), not the <video> element itself — make sure your controls are rendered as children of Media.View so they stay visible in fullscreen.

Media.Captions doesn't toggle captions yet The captions button currently renders as a static icon and isn't wired to a <track> element's mode — treat it as a UI placeholder until caption-track wiring lands.

Video won't play / throws a CORS or decode error Confirm the src is reachable and served with permissive CORS headers if it's cross-origin, and that the browser supports the container/codec. Autoplay without a prior user gesture will also be blocked by the browser regardless of this library.