# 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](https://videojs.org/docs/framework/react/guides/ui-components).
- **Restyle a control**: use CSS custom properties and data attributes. See [UI components](https://videojs.org/docs/framework/react/guides/ui-components).
- **Rearrange or remove controls**: add the skin source to your project, then modify it. See [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins#style-skin-source).

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>`](https://videojs.org/docs/framework/react/reference/components/player) to access state. Place it inside [`<Container>`](https://videojs.org/docs/framework/react/reference/components/player-container) if it should also participate in fullscreen and respond to user activity:

```tsx
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.

**SkipIntroButton.tsx**

```tsx
// 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>
  );
}
```

**SkipIntroButton.css**

```css
.skip-intro-button {
  position: absolute;
  bottom: 5rem;
  right: 1rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms;
}

.skip-intro-button[data-visible] {
  opacity: 1;
  pointer-events: auto;
}
```

Your component needs to be inside a player, such as [`<VideoPlayer>`](https://videojs.org/docs/framework/react/reference/components/player), to access state. Place it inside [`<Container>`](https://videojs.org/docs/framework/react/reference/components/player-container) if it should also participate in fullscreen and respond to user activity:

```tsx
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](https://videojs.org/docs/framework/react/guides/features). Each feature exposes a set. Here are some features you might reach for first:

| State | Actions | Feature |
| --- | --- | --- |
| `paused`, `ended` | `play()`, `pause()` | [Playback](https://videojs.org/docs/framework/react/reference/api/feature-playback) |
| `currentTime`, `duration` | `seek()` | [Time](https://videojs.org/docs/framework/react/reference/api/feature-time) |
| `volume`, `muted` | `setVolume()`, `toggleMuted()` | [Volume](https://videojs.org/docs/framework/react/reference/api/feature-volume) |
| `fullscreen` | `requestFullscreen()`, `exitFullscreen()` | [Fullscreen](https://videojs.org/docs/framework/react/reference/api/feature-fullscreen) |

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

Access state and actions with the [`usePlayer`](https://videojs.org/docs/framework/react/reference/api/use-player) 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`](https://videojs.org/docs/framework/react/reference/api/create-player) — the escape hatch for custom feature sets — use the `usePlayer` it returns instead.

```tsx
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.

- [Learn more about accessibility in video players](https://videojs.org/docs/framework/react/guides/accessibility)

## 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](https://videojs.org/docs/framework/react/guides/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](https://videojs.org/docs/framework/react/guides/ui-components).

### Restyle a control

Use CSS custom properties and data attributes. See [UI components](https://videojs.org/docs/framework/react/guides/ui-components).

### Rearrange or remove controls

Add the skin source to your project and modify it. See [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins#style-skin-source).

## 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.

## Related pages

### API

- [usePlayer](https://videojs.org/docs/framework/react/reference/api/use-player): Hook to access the player store from within a Player
- [createPlayer](https://videojs.org/docs/framework/react/reference/api/create-player): Factory function that creates a typed Player component and hooks
- [Container](https://videojs.org/docs/framework/react/reference/components/player-container): The player's visual and interaction surface for layout, fullscreen, focus, and user activity.

### Guides

- [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins): Style a packaged Video.js skin or add its source to change controls, layout, styles, and interactions

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
