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
<Media.Provider> context.d.ts files, no @types package neededtsup, with "sideEffects": falseInstall 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:
"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.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. |
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."sideEffects": false is set, so unused components are dropped by bundlers that support tree-shaking.className on any component.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.