Skip to content

Latest commit

 

History

History
251 lines (183 loc) · 7.94 KB

File metadata and controls

251 lines (183 loc) · 7.94 KB
title Build your own UI component
description Create custom player controls that read state, dispatch actions, and stay accessible.

import FrameworkCase from '@/components/docs/FrameworkCase.astro'; import DocsLink from '@/components/docs/DocsLink.astro';

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

You might not need a custom component

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

<FrameworkCase frameworks={["react"]}>

  • 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 — eject a skin and modify it. See Customize skins.

<FrameworkCase frameworks={["html"]}>

  • Restyle a control — use CSS custom properties and data attributes. See UI components.
  • Rearrange or remove controls — eject a skin and modify it. See Customize skins.

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

Use player state and actions

Each feature exposes state properties and actions. Common features:

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

Browse the full list in the Features section of the sidebar.

Each feature also has an availability property ('available', 'unavailable', or 'unsupported') for hiding controls the platform does not support. See Features for details.

<FrameworkCase frameworks={["react"]}>

Access state and actions with Player.usePlayer:

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

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

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

<FrameworkCase frameworks={["html"]}>

Access state and actions with PlayerController and a feature selector:

import { PlayerController, playerContext, selectPlayback } from '@videojs/html';

// Subscribe to a feature — triggers update() when its state changes
#playback = new PlayerController(this, playerContext, selectPlayback);

// In update():
const playback = this.#playback.value;
if (playback?.paused) {
  playback.play();
}

Each selector returns both state and actions for that feature. Use separate controllers when you need multiple features (the full example demonstrates this).

Without a selector, PlayerController returns the full store without subscribing to changes:

#store = new PlayerController(this, playerContext);

// Call any action
this.#store.value.play();
this.#store.value.setVolume(0.5);

Place your component in the player

<FrameworkCase frameworks={["react"]}>

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

<Player.Provider>
  <Player.Container>
    <VideoSkin>
      <Video src="video.mp4" />
    </VideoSkin>
    <SkipIntroButton /> {/* Your custom component */}
  </Player.Container>
</Player.Provider>

<FrameworkCase frameworks={["html"]}>

Your element needs to be inside <video-player> to access state:

<video-player>
  <video-skin>
    <video slot="media" src="video.mp4"></video>
  </video-skin>
  <skip-intro-button></skip-intro-button> <!-- Your custom element -->
</video-player>

Extend ReactiveElement from @videojs/element so PlayerController can schedule DOM updates when state changes:

import { ReactiveElement } from '@videojs/element';
import { PlayerController, playerContext, selectTime } from '@videojs/html';

class SkipIntroButtonElement extends ReactiveElement {
  #player = new PlayerController(this, playerContext, selectTime);

  update() {
    super.update();
    // React to state changes here
  }
}

customElements.define('skip-intro-button', SkipIntroButtonElement);

Make it accessible

Use semantic HTML elements (<button>, not <div>), add ARIA attributes where needed, and support keyboard interaction.

TODO: Link to concepts/accessibility once that page is merged

<FrameworkCase frameworks={["react"]}>

For button-like controls, the useButton hook handles keyboard activation (Enter and Space) and ARIA attributes.

Full example

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

<FrameworkCase frameworks={["react"]}>

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

  const visible = currentTime < 30 && !paused;

  return (
    <button
      onClick={() => store.seek(30)}
      aria-label="Skip intro"
      style={{
        position: 'absolute',
        bottom: '5rem',
        right: '1rem',
        opacity: visible ? 1 : 0,
        pointerEvents: visible ? 'auto' : 'none',
        transition: 'opacity 200ms',
      }}
    >
      Skip intro
    </button>
  );
}

<FrameworkCase frameworks={["html"]}>

import { ReactiveElement } from '@videojs/element';
import { PlayerController, playerContext, selectTime, selectPlayback } from '@videojs/html';

class SkipIntroButtonElement extends ReactiveElement {
  static tagName = 'skip-intro-button';

  #time = new PlayerController(this, playerContext, selectTime);
  #playback = new PlayerController(this, playerContext, selectPlayback);

  connectedCallback() {
    super.connectedCallback();
    this.setAttribute('role', 'button');
    this.setAttribute('aria-label', 'Skip intro');
    this.setAttribute('tabindex', '0');
    this.addEventListener('click', this.#handleActivate);
    this.addEventListener('keydown', this.#handleKeydown);
  }

  update() {
    super.update();
    const time = this.#time.value;
    const playback = this.#playback.value;
    if (!time || !playback) return;

    const visible = time.currentTime < 30 && !playback.paused;
    this.toggleAttribute('data-visible', visible);
    this.style.opacity = visible ? '1' : '0';
    this.style.pointerEvents = visible ? 'auto' : 'none';
  }

  #handleActivate = () => {
    this.#time.value?.seek(30);
  };

  #handleKeydown = (event: KeyboardEvent) => {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.#handleActivate();
    }
  };
}

customElements.define('skip-intro-button', SkipIntroButtonElement);

Style it with CSS:

skip-intro-button {
  position: absolute;
  bottom: 5rem;
  right: 1rem;
  transition: opacity 200ms;
}