Headless media primitives for React

The headless
media player.

React Media Kit gives you the playback state, the keyboard behaviour and the accessibility work as unstyled parts. Compose them into a player that looks like your product.

Space play/pause · ← → seek · M mute · F fullscreen

What you get

Behaviour is hard. Styling should not be.

A set of compound components covers the full surface of a media element, handling the tricky behavioral details so you don't have to, and handing you the state to render however you want, with no markup or styling assumptions baked in.

Headless by default

No stylesheet ships with the kit. Parts render a bare element, forward your className and ref, and expose state as data attributes.

Keyboard shortcuts built in

Playback, seeking, volume and fullscreen all respond to the shortcuts people already expect, with no extra wiring.

One external store

Components subscribe to only the slice of state they need, so updates stay fast no matter how deep the tree gets.

Seeking that feels right

Dragging, a buffered track and clamped bounds are already handled, so you can focus purely on styling.

Fullscreen and PiP

Cross-browser fullscreen and picture-in-picture come ready to use, with an active state you can style.

Small and quiet

Tree-shakeable ES modules, zero runtime dependencies beyond React.

Anatomy

Compose your player, piece by piece

One root component sets up the shared player state. Everything else is optional, so you only compose the pieces you actually need.

Read the API reference
Player.tsx
import { Player, Video, Controls, Seekbar, PlayButton, TimeDisplay } from 'react-media-kit'

function VideoPlayer({ src }) {
  return (
    <Player.Root>
      <Player.Container>
        <Video.Root src={src} />

        <Controls.Root>
          <Seekbar.Root>
            <Seekbar.Track>
              <Seekbar.Progress />
              <Seekbar.Buffer />
              <Seekbar.Thumb />
            </Seekbar.Track>
          </Seekbar.Root>

          <PlayButton.Root>Play</PlayButton.Root>

          <TimeDisplay.Root>
            <TimeDisplay.Toggle>
              <TimeDisplay.Timer />
              <span>/</span>
              <TimeDisplay.Duration />
            </TimeDisplay.Toggle>
          </TimeDisplay.Root>
        </Controls.Root>
      </Player.Container>
    </Player.Root>
  )
}
import { useEffect, useRef } from 'react'
import Hls from 'hls.js'
import { Player, Video, Controls, PlayButton } from 'react-media-kit'

function HlsPlayer({ src }) {
  const videoRef = useRef(null)

  useEffect(() => {
    const video = videoRef.current
    if (!video || !Hls.isSupported()) return

    const hls = new Hls()
    hls.loadSource(src)
    hls.attachMedia(video)

    return () => hls.destroy()
  }, [src])

  return (
    <Player.Root>
      <Player.Container>
        <Video.Root ref={videoRef} />

        <Controls.Root>
          <PlayButton.Root>Play</PlayButton.Root>
        </Controls.Root>
      </Player.Container>
    </Player.Root>
  )
}
import { usePlayer, usePlayerControls, Seekbar, TimeDisplay } from 'react-media-kit'

function CustomPlayButton() {
  const isPlaying = usePlayer((s) => s.isPlaying)
  const { toggle } = usePlayerControls()

  return (
    <button
      onClick={toggle}
      className="rounded-full p-2 text-white/70 hover:text-white data-[playing]:text-emerald-400"
    >
      {isPlaying ? 'Pause' : 'Play'}
    </button>
  )
}

function CustomSeekbar() {
  return (
    <Seekbar.Root className="group relative h-1 w-full cursor-pointer">
      <Seekbar.Track className="h-full rounded-full bg-white/15">
        <Seekbar.Buffer className="h-full rounded-full bg-white/30" />
        <Seekbar.Progress className="h-full rounded-full bg-emerald-400" />
        <Seekbar.Thumb className="size-3 rounded-full bg-white opacity-0 transition group-hover:opacity-100 data-[dragging]:opacity-100" />
      </Seekbar.Track>
    </Seekbar.Root>
  )
}

function CustomTimeDisplay() {
  return (
    <TimeDisplay.Root className="font-mono text-xs text-white/50 data-[live]:text-red-400">
      <TimeDisplay.Toggle>
        <TimeDisplay.Timer />
        <span>/</span>
        <TimeDisplay.Duration />
      </TimeDisplay.Toggle>
    </TimeDisplay.Root>
  )
}

Hooks

Skip the parts entirely

If the compound components are more structure than you need, drop to the two hooks they're built on. Everything a player does is reachable from these.

usePlayer()
usePlayer(selector?)

Reads the live player state - playback status, current time, duration, buffered ranges, volume and rate. Pass a selector and the component re-renders only when that slice changes.

isPlayingcurrentTimedurationbufferedvolumeisMutedplaybackRate
const isMuted = usePlayer((s) => s.isMuted)

<span>{isMuted && "Muted"}</span>
usePlayerControls()
usePlayerControls()

Returns the imperative side: play, pause, seeking, volume and rate setters. The object identity is stable, so it's safe in dependency arrays and event handlers.

toggleseekskiptoggleMutetoggleFullscreensetPlaybackRate
const { toggle, seek, setPlaybackRate } = usePlayerControls()

<button onClick={toggle}>Play</button>
<button onClick={() => seek(30)}>30s</button>

React Media Kit brings the behavior.
Bring your own styles.

MIT licensed, fully typed, and built for React 19+. Install it and start composing.