Guides

Custom Controls

Replace the built-in UI with a completely custom control layout.

Custom Controls

PlayerKit's built-in UI is highly customizable via CSS (see Theming), but sometimes you need a radically different DOM layout.

You can completely replace the control bar by providing a renderControls function to the player component.


Basic Replacement

If you pass renderControls, the default control bar is destroyed, and your React element is injected into the player overlay instead.

import { Player } from "@playerkit/react";

function CustomPlayer() {
  return (
    <Player
      src="https://example.com/stream.m3u8"
      renderControls={({ player, state, formatTime }) => (
        <div style={{ display: "flex", gap: "10px", padding: "10px", background: "#000" }}>
          <button onClick={() => player?.togglePlay()}>
            {state?.isPlaying ? "Pause" : "Play"}
          </button>
          
          <span style={{ color: "white" }}>
            {formatTime(state?.currentTime ?? 0)}
          </span>
        </div>
      )}
    />
  );
}

The renderControls Payload

Your callback receives a single object with the following properties:

PropertyTypeDescription
playerPlayerControls | nullThe API for commanding the video (play, pause, seek, setVolume)
statePlayerSnapshot | nullThe reactive state of the video (isPlaying, currentTime, volume)
progressnumberA pre-calculated 0-1 float of playback progress
bufferednumberA pre-calculated 0-1 float of buffered progress
seekRelative(direction: -1 | 1) => voidHelper to jump forward/backward by seekStep seconds
formatTime(seconds: number) => stringHelper to format raw seconds into MM:SS or HH:MM:SS

Building a Range Slider

A common requirement is building a custom seek bar. Here is how to wire up a native <input type="range"> correctly:

<Player
  src="https://example.com/stream.m3u8"
  renderControls={({ player, state }) => {
    
    // Ensure we don't pass NaN to the input
    const duration = state?.duration || 100;
    const current = state?.currentTime || 0;

    return (
      <div className="my-custom-controls">
        <input
          type="range"
          min={0}
          max={duration}
          step={0.1}
          value={current}
          onChange={(e) => {
            const newTime = Number(e.target.value);
            player?.seek(newTime);
          }}
          onPointerDown={() => {
            // Optional: Pause while dragging for a smoother UX
            if (state?.isPlaying) player?.pause();
          }}
          onPointerUp={() => {
            // Optional: Resume play after dropping
            player?.play();
          }}
        />
      </div>
    );
  }}
/>

Hybrid Approach (Headless Hooks)

If renderControls feels too restrictive because you want controls completely outside the video container, you should use the Headless Hooks instead (e.g. useHlsPlayer).

See the React Hooks guide for how to build a 100% custom UI from the ground up without using the wrapper components.

On this page