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
<Media.Provider> context.d.ts files, no @types package neededtsup — only the bundled stylesheet is marked as a side effectInstall the package with your package manager of choice:
mediakit-player expects react and react-dom (>=17) as peer dependencies, provided by your app:
npm install react react-dom
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>
);
}
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>;
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>
);
}
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.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. |
react and react-dom >=17 must already be present in your app; they are not bundled.dist/index.js) and CJS (dist/index.cjs) builds via tsup, with bundled .d.ts types, so no @types/mediakit-player package is needed.dist/index.css) is marked as a side effect so it survives.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".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.<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>
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>.
"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.