Skip to content

ReferenceLayout

video-player

The state boundary — creates a store and broadcasts it to all descendants.

The <video-player> element is the state boundary of your player. It creates a store and makes it available to every element inside it via context. Every player needs exactly one.

<video-player>
  <!-- Everything inside can access the player store -->
  <media-container>
    <video src="video.mp4"></video>
  </media-container>
</video-player>

How it’s created

For most users, importing from @videojs/html/video/player registers a ready-to-use <video-player> element with the standard video features (bundled as a preset):

import '@videojs/html/video/player';
<video-player>
  <media-container>
    <video src="video.mp4"></video>
  </media-container>
</video-player>

If you need a custom feature set or tag name, use createPlayer() to get the configured PlayerElement. Extend ContainerElement separately when you need custom container behavior:

import { ContainerElement, createPlayer } from '@videojs/html';
import { videoFeatures } from '@videojs/html/video';

const { PlayerElement: MyPlayer } = createPlayer({ features: videoFeatures });

class MyContainer extends ContainerElement {}
customElements.define('my-player', MyPlayer);
customElements.define('my-container', MyContainer);

What lives inside it

Everything that needs player state goes inside <video-player>: skins, containers, UI components, and your own custom components. Anything inside can access the store.

<video-player>
  <video-skin>               <!-- skin — includes container + controls -->
    <video src="..."></video> <!-- media element -->
  </video-skin>
  <my-custom-overlay></my-custom-overlay> <!-- your own element — can use PlayerController -->
</video-player>

No visual presence

Packaged skins make <video-player> boxless with display: contents. Width, height, positioning, transforms, and getBoundingClientRect() do not describe a visible player box there. Put layout and measurements on the <media-container> instead.

If you omit <video-skin> and import only the player and media elements, those skin styles are not loaded. The player element still owns state, not layout, so add and style a <media-container> instead of using the player itself as the layout surface.

Accessing state

Use the preset’s typed PlayerController to subscribe to store state from any custom element inside the player:

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

class PlayPauseButton extends UIElement {
  readonly #playback = new PlayerController(this, selectPlayback);

  connectedCallback() {
    super.connectedCallback();
    this.addEventListener('click', this.#togglePlayback);
  }

  disconnectedCallback() {
    this.removeEventListener('click', this.#togglePlayback);
    super.disconnectedCallback();
  }

  readonly #togglePlayback = () => this.#playback.value?.togglePaused();
}

customElements.define('play-pause-button', PlayPauseButton);

Extended player layouts

The player’s scope can extend beyond the fullscreen target. Playlists, transcripts, sidebars, and other supplementary UI can live inside <video-player> but outside <media-container>. They still have full access to the store, but they won’t go fullscreen with the video.

<video-player>
  <media-container>
    <video src="video.mp4"></video>
    <media-controls><media-controls-content>...</media-controls-content></media-controls>  <!-- goes fullscreen with the video -->
  </media-container>

  <media-transcript></media-transcript>     <!-- outside container — still has store access -->
  <playlist-sidebar></playlist-sidebar>     <!-- outside container — still has store access -->
</video-player>