Skip to content

GuideGetting Started

Build your own UI component

Create custom player controls that read state, dispatch actions, and stay accessible.

Custom components subscribe to player state and dispatch actions, like built-in controls.

You might not need a custom component

Before building from scratch, check if an existing approach covers your use case:

  • Change what a control renders: use the render prop on any built-in component. See UI components.
  • Restyle a control: use CSS custom properties and data attributes. See UI components.
  • Rearrange or remove controls: add the skin source to your project, then modify it. See Customize skins.

Build a custom component when you need new behavior, a new state display, or integration with an external system.

Place your component in the player

Your component needs to be inside <Player> to access state. Place it inside <Container> if it should also participate in fullscreen and respond to user activity:

import { Container } from '@videojs/react';

<Player>
  <Container>
    <VideoSkin>
      <Video src="video.mp4" />
    </VideoSkin>
    <SkipIntroButton />
  </Container>
</Player>

Full example

A “skip intro” button that appears during the first 30 seconds of playback and seeks past the intro when clicked.

// The preset's usePlayer is typed for its feature bundle
import { usePlayer } from '@videojs/react/video';

export function SkipIntroButton() {
  const store = usePlayer();
  const currentTime = usePlayer((s) => s.currentTime);
  const paused = usePlayer((s) => s.paused);

  const visible = currentTime < 30 && !paused;

  return (
    <button
      className="skip-intro-button"
      onClick={() => store.seek(30)}
      aria-label="Skip intro"
      // `undefined` removes the attribute; `false` would render data-visible="false"
      data-visible={visible || undefined}
      tabIndex={visible ? 0 : -1}
    >
      Skip intro
    </button>
  );
}

Your component needs to be inside a player, such as <VideoPlayer>, to access state. Place it inside <Container> if it should also participate in fullscreen and respond to user activity:

import { Container } from '@videojs/react';
import { Video, VideoPlayer, VideoSkin } from '@videojs/react/video';
import '@videojs/react/video/skin.css';

import { SkipIntroButton } from './SkipIntroButton';

export default function App() {
  return (
    <VideoPlayer>
      <Container>
        <VideoSkin>
          <Video src="video.mp4" />
        </VideoSkin>
        <SkipIntroButton />
      </Container>
    </VideoPlayer>
  );
}

How it works

Custom components read player state and dispatch actions through features. Each feature exposes a set. Here are some features you might reach for first:

State Actions Feature
paused, ended play(), pause() Playback
currentTime, duration seek() Time
volume, muted setVolume(), toggleMuted() Volume
fullscreen requestFullscreen(), exitFullscreen() Fullscreen

The API reference lists every feature with the state and actions it adds.

Access state and actions with the usePlayer hook from your player’s preset. It knows which features that preset has, so state and actions are typed. (The standalone usePlayer export from @videojs/react returns an untyped store, so its values are unknown in TypeScript.) If you built the player with a custom feature set through createPlayer — the escape hatch for custom feature sets — use the usePlayer it returns instead.

import { usePlayer } from '@videojs/react/video';

// Subscribe to state — re-renders only when selected values change
const paused = usePlayer((s) => s.paused);
const currentTime = usePlayer((s) => s.currentTime);

// Get the store for dispatching actions (does not subscribe)
const store = usePlayer();

await store.play();
store.setVolume(0.5);
store.seek(30);

Custom controls also need real button semantics — the examples above set the accessible name and keyboard focus by hand.

Availability and constraints

  • Features are configured per player, so a feature your component asks for may not be present. Selectors return undefined for a missing feature; guard the value before using it, as the example does.
  • Volume, fullscreen, picture-in-picture, and remote playback also expose an *Availability property ('available', 'unavailable', or 'unsupported') for hiding controls the platform does not support. See Features for details.

Common variations

Before building from scratch, check if an existing approach covers your use case. Build a custom component when you need new behavior, a new state display, or integration with an external system.

Change what a built-in control renders

Use the render prop on any built-in component. See UI components.

Restyle a control

Use CSS custom properties and data attributes. See UI components.

Rearrange or remove controls

Add the skin source to your project and modify it. See Customize skins.

Troubleshooting

State and actions are typed as unknown

You’re using the standalone usePlayer export, which doesn’t know which features your player has. Import usePlayer from your player’s preset (for example @videojs/react/video), or use the hook returned by createPlayer if you built the player with a custom feature set.

The component renders but never updates

The component sits outside the player (such as <VideoPlayer>), so usePlayer has no store to subscribe to.

API

Guides