HomeProjectsComponentsNotes
Theme

Video Player

v0.1.0

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, unstyled 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 and style however you like.

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

Highlights

  • 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
  • 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, with "sideEffects": false

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:

"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

Every component accepts className and forwards remaining DOM props, so Tailwind, CSS Modules, or plain CSS all work.

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 — "sideEffects": false is set, so unused components are dropped by bundlers that support tree-shaking.
  • Styling — nothing is pre-styled beyond layout-critical classes (aspect ratio, flex positioning). Bring your own theme via className on any component.
  • 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.