# Video.js v10 — HTML API (complete) > Every HTML API page in one file (about 47k tokens). Index with descriptions: https://videojs.org/docs/framework/html/reference/api/llms.txt --- # createPlayer Factory function that creates a typed player element and controller for HTML custom elements ## Import ```ts import { createPlayer } from "@videojs/html"; import { videoFeatures } from "@videojs/html/video"; ``` `createPlayer` is the entry point for custom HTML player compositions. It accepts a `features` array and returns the configured `PlayerElement`, a [`PlayerController`](https://videojs.org/docs/framework/html/reference/api/player-controller) already bound to that player, and `playerContext` as a lower-level escape hatch. Import `ContainerElement` separately when you need a custom container. **player.ts** ```ts import { createPlayer, UIElement, selectPlayback } from '@videojs/html'; import { videoFeatures } from '@videojs/html/video'; const { PlayerElement: VideoPlayerElement, PlayerController } = createPlayer({ features: videoFeatures, }); customElements.define('video-player', VideoPlayerElement); // Control element with selector class PlayButton extends UIElement { #playback = new PlayerController(this, selectPlayback); } ``` `PlayerElement` and `PlayerController` are scoped to the created player’s feature set. The element owns its store and attachment lifecycle; the configured controller does not need a context argument. `ContainerElement` consumes the shared player context, so the same class works with any created player. Use split elements (recommended for reusable skins/layouts): **player.ts** ```ts import { ContainerElement, createPlayer } from '@videojs/html'; import { videoFeatures } from '@videojs/html/video'; const { PlayerElement: PlayerRoot } = createPlayer({ features: videoFeatures }); class PlayerRegion extends ContainerElement {} ``` ## Examples ### Basic Usage **index.html** ```html ``` **index.css** ```css .demo-video-player media-container { position: relative; } .demo-video-player media-container video { width: 100%; } .demo-video-player demo-play-toggle { position: absolute; bottom: 10px; left: 10px; } .media-play-button { padding-block: 8px; padding-inline: 20px; font: inherit; color: black; cursor: pointer; background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 9999px; backdrop-filter: blur(10px); } .media-play-button .paused { display: none; } .media-play-button .playing { display: none; } .demo-video-player demo-play-toggle[data-paused] .paused { display: inline; } .demo-video-player demo-play-toggle:not([data-paused]) .playing { display: inline; } ``` **index.ts** ```ts import { createPlayer, selectPlayback, UIElement } from '@videojs/html'; import { videoFeatures } from '@videojs/html/video'; import '@videojs/html/ui/container'; const { PlayerElement: VideoPlayerElement, PlayerController } = createPlayer({ features: videoFeatures, }); class PlayToggle extends UIElement { static readonly tagName = 'demo-play-toggle'; readonly #player = new PlayerController(this, selectPlayback); #disconnect: AbortController | null = null; override connectedCallback(): void { super.connectedCallback(); this.#disconnect = new AbortController(); this.querySelector('button')?.addEventListener('click', this.#toggle, { signal: this.#disconnect.signal }); } override disconnectedCallback(): void { super.disconnectedCallback(); this.#disconnect?.abort(); this.#disconnect = null; } protected override update(changed: Map): void { super.update(changed); const state = this.#player.value; if (!state) return; this.toggleAttribute('data-paused', state.paused); this.toggleAttribute('data-ended', state.ended); } #toggle = (): void => { const state = this.#player.value; if (!state) return; if (state.paused) state.play(); else state.pause(); }; } customElements.define('demo-video-player', VideoPlayerElement); customElements.define(PlayToggle.tagName, PlayToggle); ``` ## API Reference ### Video `createPlayer(config): CreatePlayerResult` Creates a typed HTML player class and bound controller. #### Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `config` (required) | `{ features: [PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, typeof metadataFeature] }` | — | Player configuration with features. | #### Return Value | Property | Type | Description | | --- | --- | --- | | `PlayerElement` | `typeof UIElement & (new (...args: any[]) => PlayerElement)` | Configured player element class that owns the store and attachment lifecycle. | | `PlayerController` | `{ new (host: ReactiveControllerHost & HTMLElement): PlayerController; new (host: ReactiveControllerHost & HTMLElement, selector: Selector): PlayerController }` | Player controller bound to this player's context. | | `playerContext` | `Context` | Context that carries the player store to descendant elements. | ### Audio `createPlayer(config): CreatePlayerResult` Creates a typed HTML audio player class and bound controller. #### Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `config` (required) | `{ features: [PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, PlayerFeature, typeof metadataFeature] }` | — | Player configuration with features. | #### Return Value | Property | Type | Description | | --- | --- | --- | | `PlayerElement` | `typeof UIElement & (new (...args: any[]) => PlayerElement)` | Configured player element class that owns the store and attachment lifecycle. | | `PlayerController` | `{ new (host: ReactiveControllerHost & HTMLElement): PlayerController; new (host: ReactiveControllerHost & HTMLElement, selector: Selector): PlayerController }` | Player controller bound to this player's context. | | `playerContext` | `Context` | Context that carries the player store to descendant elements. | ### Generic `createPlayer(config): CreatePlayerResult>` Creates a typed HTML player class with custom features. #### Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `config` (required) | `{ features: Features }` | — | Player configuration with features. | #### Return Value | Property | Type | Description | | --- | --- | --- | | `PlayerElement` | `typeof UIElement & (new (...args: any[]) => PlayerElement>)` | Configured player element class that owns the store and attachment lifecycle. | | `PlayerController` | `{ new (host: ReactiveControllerHost & HTMLElement): PlayerController>; new (host: ReactiveControllerHost & HTMLElement, selector: Selector['state'], Result>): PlayerController, Result> }` | Player controller bound to this player's context. | | `playerContext` | `Context>` | Context that carries the player store to descendant elements. | --- # PlayerController Reactive controller for accessing player store state in HTML custom elements ## Import ```ts import { PlayerController } from "@videojs/html/video"; ``` `PlayerController` is a reactive controller that consumes the player store from `playerContext`. Without a selector it returns the store instance directly (no subscription — use this for actions). With a selector it returns the selected value and subscribes to changes, triggering a host update on shallow-equal change. Access the current value via `.value`, which returns `undefined` until connected to a player. The controller exported by each preset, or returned by [`createPlayer`](https://videojs.org/docs/framework/html/reference/api/html-create-player), is already bound to that player’s context, so pass only the host and optional selector. The lower-level `PlayerController` exported from `@videojs/html` accepts the context as its second argument; its constructor signatures are documented below. ```ts import { UIElement, selectPlayback } from '@videojs/html'; import { PlayerController } from '@videojs/html/video'; class PlayButtonElement extends UIElement { readonly #playback = new PlayerController(this, selectPlayback); } ``` The host must be a reactive controller host, such as a subclass of [`UIElement`](https://videojs.org/docs/framework/html/reference/api/ui-element), so the controller can request updates. ## Player context `playerContext` is the context every player element provides to its descendants, whether the element comes from a preset or from [`createPlayer`](https://videojs.org/docs/framework/html/reference/api/html-create-player). `PlayerController` requests it from its host when the host connects, and the nearest ancestor player answers with its store. An element outside a player receives nothing, so `value` stays `undefined`. Pass `playerContext` to the `PlayerController` exported from `@videojs/html`: ```ts import { PlayerController, playerContext, selectPlayback, UIElement } from "@videojs/html"; class PlayButtonElement extends UIElement { readonly #playback = new PlayerController(this, playerContext, selectPlayback); } ``` With a selector, the selector types the value. Without one, `value` is typed as the base player store, with no feature state or actions, because `playerContext` does not know the player’s features. For typed access, use the `PlayerController` that `createPlayer` returns, which is already bound to its player, or pass the context `createPlayer` returns, which is the same context as `playerContext` typed for its feature set: ```ts import { createPlayer, PlayerController, UIElement } from "@videojs/html"; import { videoFeatures } from "@videojs/html/video"; const { playerContext } = createPlayer({ features: videoFeatures }); class VolumeResetElement extends UIElement { readonly #store = new PlayerController(this, playerContext); reset() { this.#store.value?.setVolume(1); } } ``` ## Examples ### Basic Usage **index.html** ```html
Paused: Yes | Time: 0.0s | Volume: 100%
``` **index.css** ```css .demo-ctrl-player media-container { position: relative; } .demo-ctrl-player media-container video { width: 100%; } .panel { display: flex; gap: 16px; align-items: center; padding: 12px; background: rgba(0, 0, 0, 0.05); border-top: 1px solid rgba(0, 0, 0, 0.1); } .actions { display: flex; gap: 6px; } .actions button { padding: 4px 12px; font-size: 0.8125rem; color: #111827; cursor: pointer; background: white; border: 1px solid #ccc; border-radius: 6px; } .state { font-size: 0.8125rem; font-variant-numeric: tabular-nums; color: #374151; } ``` **index.ts** ```ts import { createPlayer, selectPlayback, selectTime, selectVolume, UIElement } from '@videojs/html'; import { videoFeatures } from '@videojs/html/video'; import '@videojs/html/ui/container'; const { PlayerElement: DemoPlayerElement, PlayerController } = createPlayer({ features: videoFeatures, }); class PlayerActions extends UIElement { static readonly tagName = 'demo-ctrl-actions'; readonly #player = new PlayerController(this); #disconnect: AbortController | null = null; override connectedCallback(): void { super.connectedCallback(); this.#disconnect = new AbortController(); const signal = this.#disconnect.signal; const bind = (selector: string, action: () => void) => { this.querySelector(selector)?.addEventListener('click', action, { signal }); }; bind('.play', () => this.#player.value?.play()); bind('.pause', () => this.#player.value?.pause()); bind('.volume', () => this.#player.value?.setVolume(0.5)); } override disconnectedCallback(): void { super.disconnectedCallback(); this.#disconnect?.abort(); this.#disconnect = null; } } class PlayerState extends UIElement { static readonly tagName = 'demo-ctrl-state'; readonly #playback = new PlayerController(this, selectPlayback); readonly #time = new PlayerController(this, selectTime); readonly #volume = new PlayerController(this, selectVolume); protected override update(changed: Map): void { super.update(changed); const playback = this.#playback.value; const time = this.#time.value; const volume = this.#volume.value; if (!playback) return; const el = this.querySelector('.text'); if (el) { el.textContent = `Paused: ${playback.paused ? 'Yes' : 'No'} | Time: ${(time?.currentTime ?? 0).toFixed(1)}s | Volume: ${Math.round((volume?.volume ?? 0) * 100)}%`; } } } customElements.define('demo-ctrl-player', DemoPlayerElement); customElements.define(PlayerActions.tagName, PlayerActions); customElements.define(PlayerState.tagName, PlayerState); ``` ## API Reference ### Without Selector `new PlayerController(host, context)` #### Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `host` (required) | `{ addController(controller: ReactiveController): void; removeController(controller: ReactiveController): void; requestUpdate(): void; updateComplete: Promise } & HTMLElement` | — | The host element that owns this controller. | | `context` (required) | `Context` | — | Player context to resolve the store from. | #### Return Value | Property | Type | | --- | --- | | `value` | `Result \| undefined` | | `displayName` | `string \| undefined` | ### With Selector `new PlayerController(host, context, selector)` #### Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `host` (required) | `{ addController(controller: ReactiveController): void; removeController(controller: ReactiveController): void; requestUpdate(): void; updateComplete: Promise } & HTMLElement` | — | The host element that owns this controller. | | `context` (required) | `Context` | — | Player context to resolve the store from. | | `selector` (required) | `{ (state: Store['state']): Result; displayName?: string }` | — | Derives a value from the player store state. | #### Return Value | Property | Type | | --- | --- | | `value` | `Result \| undefined` | | `displayName` | `string \| undefined` | --- # UIElement Base class for custom elements that read player state through reactive controllers `UIElement` is the base class for Video.js custom elements, and the one the built-in UI elements extend. It adds reactive properties, a batched update cycle, reactive controllers, and deferred destruction to `HTMLElement`, and renders into the light DOM: the subclass writes its own DOM in `update()`. `UIElement` extends `DestroyMixin(ReactiveElement)` from Video.js’s own element base, not from Lit. Its reactive properties and update cycle follow the model of Lit’s `ReactiveElement`, with the same names: `static properties`, `requestUpdate()`, `willUpdate()`, `update()`, `updated()`, `updateComplete`, and reactive controllers. It has no templates, shadow root, or `static styles`, no decorators, and no reflection of properties to attributes. `DestroyMixin` adds the [destruction](https://videojs.org/docs/framework/html/reference/api/ui-element#destruction) lifecycle and the `hostDestroyed` controller hook, which Lit doesn’t have. ## Import ```ts import { UIElement } from "@videojs/html"; ``` ## Usage Extend `UIElement`, add a [`PlayerController`](https://videojs.org/docs/framework/html/reference/api/player-controller) for the player state the element needs, and write DOM in `update()`. The controller requests an update whenever its selected state changes. ```ts import { PlayerController, playerContext, selectTime, UIElement, type PropertyValues } from "@videojs/html"; class CurrentTimeElement extends UIElement { readonly #time = new PlayerController(this, playerContext, selectTime); protected override update(changed: PropertyValues): void { super.update(changed); const time = this.#time.value; if (!time) return; this.textContent = time.currentTime.toFixed(1); } } customElements.define("current-time", CurrentTimeElement); ``` The element must be a descendant of a player element, such as ``, to receive state. Until then, and when the player lacks the selected feature, `value` is `undefined`. `playerContext` is the context every player element provides to its descendants. See [Player context](https://videojs.org/docs/framework/html/reference/api/player-controller#player-context) for how it resolves and how it is typed. The `PlayerController` exported by a preset, or returned by [`createPlayer`](https://videojs.org/docs/framework/html/reference/api/html-create-player), is already bound to that player’s context and takes only the host and an optional selector. ## Reactive properties Declare reactive properties in `static properties` and initialize them as class fields. Setting a declared property to a new value (compared with `Object.is`) schedules an update. Each declaration also observes a matching attribute. ```ts class SeekByElement extends UIElement { static override properties = { seconds: { type: Number }, skipTo: { type: Number, attribute: "skip-to" }, disabled: { type: Boolean }, }; seconds = 10; skipTo = 0; disabled = false; } ``` A `PropertyDeclaration` has two options: | Option | Default | Description | | --- | --- | --- | | `type` | `String` | Converts the attribute value. `String` passes the value through (`null` when the attribute is removed), `Number` parses it (`null` when removed), and `Boolean` is `true` while the attribute is present. | | `attribute` | The property name | The observed attribute. HTML lowercases attribute names, so give camelCase properties a lowercase attribute, such as `{ attribute: "skip-to" }`. | Attributes update properties; properties do not reflect back to attributes. Set attributes yourself in `update()` when styling or accessibility depends on them. Values assigned before the element is defined, or before it connects, are replayed through the reactive accessors on first connection, so they win over the class-field defaults. A subclass of an element that already declares properties must spread the parent’s declarations: ```ts class LabeledSeekByElement extends SeekByElement { static override properties = { ...SeekByElement.properties, icon: { type: String }, }; icon = ""; } ``` ## Update cycle Property changes and `requestUpdate()` calls made in the same task are batched into one update, which runs in a microtask. The first update waits until the element connects. Each update calls, in order: 1. `willUpdate(changed)` — compute values derived from other properties. Changes made here join the current update. 2. `hostUpdate()` on each controller. 3. `update(changed)` — write the element’s DOM. Property changes made here do not schedule another update. 4. `hostUpdated()` on each controller. 5. `firstUpdated(changed)` — once, after the first update. 6. `updated(changed)` — after every update. Property changes made here, or in `firstUpdated`, schedule another update. `changed` is a `Map` from each changed property name to its previous value. Call `super` when overriding any of these methods. | Member | Description | | --- | --- | | `requestUpdate(name?, oldValue?)` | Schedules an update. Call it with no arguments when the element depends on state outside its reactive properties. | | `updateComplete` | Promise that resolves when the pending update finishes. It resolves to `false` when that update scheduled another one. | | `hasUpdated` | `true` after the first update. | | `isUpdatePending` | `true` from when an update is scheduled until its `update()` call returns. | ## Controllers A reactive controller is an object that hooks into its host element’s lifecycle. Controllers such as `PlayerController` add themselves to the host when they are constructed. ### ReactiveController `ReactiveController` is the interface a controller implements. Every hook is optional: | Hook | Called when | | --- | --- | | `hostConnected()` | The element connects, or immediately when the controller is added to a connected element | | `hostDisconnected()` | The element disconnects | | `hostUpdate()` | During each update, before `update()` | | `hostUpdated()` | During each update, after `update()` | | `hostDestroyed()` | The element is destroyed | ### ReactiveControllerHost `ReactiveControllerHost` is the interface of the element a controller attaches to. `UIElement` implements it, so a controller whose constructor accepts a `ReactiveControllerHost` works with any `UIElement` subclass. | Member | Description | | --- | --- | | `addController(controller)` | Adds a controller so the element calls its hooks. Calls `hostConnected()` immediately when the element is connected. | | `removeController(controller)` | Removes a controller. Its hooks stop being called; `hostDisconnected()` is not called. | | `requestUpdate()` | Schedules an update of the element. | | `updateComplete` | Promise that resolves when the element’s pending update finishes. It resolves to `false` when that update scheduled another one. | ```ts import type { ReactiveController, ReactiveControllerHost } from "@videojs/html"; class ClockController implements ReactiveController { now = Date.now(); readonly #host: ReactiveControllerHost; #timer = 0; constructor(host: ReactiveControllerHost) { this.#host = host; host.addController(this); } hostConnected(): void { this.#timer = window.setInterval(() => { this.now = Date.now(); this.#host.requestUpdate(); }, 1000); } hostDisconnected(): void { this.#stop(); } // `destroy()` can run while the element is still connected. hostDestroyed(): void { this.#stop(); } #stop(): void { window.clearInterval(this.#timer); } } ``` ## Destruction When the element disconnects, it waits two animation frames and then destroys itself if it is still disconnected. Moving the element within the document, or a framework reordering it, therefore keeps it alive. Destruction calls `hostDestroyed()` on every controller. A destroyed element stops updating and ignores later connections. | Member | Description | | --- | --- | | `destroy()` | Destroys the element now. Calling it again has no effect. | | `destroyed` | `true` after destruction. | | `destroyCallback()` | Runs once on destruction. Override it, calling `super.destroyCallback()`, to release resources the element owns. | Add the `keep-alive` attribute to skip automatic destruction, for example when you detach an element and insert it again later. Call `destroy()` yourself when you are done with it. Release listeners and observers that the element starts on connection in `disconnectedCallback()`, calling `super.disconnectedCallback()`, because an element can connect and disconnect many times before it is destroyed. ## Examples ### Basic Usage A seek button with a reactive `seconds` property, set from its attribute. **index.html** ```html
``` **index.css** ```css .html-ui-element-basic__container { position: relative; } .html-ui-element-basic__container video { width: 100%; } .html-ui-element-basic__controls { position: absolute; bottom: 12px; left: 12px; display: flex; gap: 8px; } .html-ui-element-basic__button { padding: 6px 12px; font-size: 0.875rem; font-variant-numeric: tabular-nums; color: white; cursor: pointer; background: rgba(0, 0, 0, 0.6); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 6px; } .html-ui-element-basic__button:disabled { cursor: default; opacity: 0.5; } ``` **index.ts** ```ts import '@videojs/html/video/player'; import '@videojs/html/ui/container'; import { PlayerController, playerContext, type PropertyValues, selectTime, UIElement } from '@videojs/html'; class SeekByElement extends UIElement { static readonly tagName = 'demo-seek-by'; static override properties = { seconds: { type: Number }, }; seconds = 10; readonly #time = new PlayerController(this, playerContext, selectTime); #disconnect: AbortController | null = null; override connectedCallback(): void { super.connectedCallback(); this.#disconnect = new AbortController(); this.querySelector('button')?.addEventListener('click', this.#seek, { signal: this.#disconnect.signal }); } override disconnectedCallback(): void { super.disconnectedCallback(); this.#disconnect?.abort(); this.#disconnect = null; } protected override update(changed: PropertyValues): void { super.update(changed); const button = this.querySelector('button'); if (!button) return; if (changed.has('seconds')) { const amount = Math.abs(this.seconds); button.textContent = `${this.seconds < 0 ? '-' : '+'}${amount}s`; button.ariaLabel = `Seek ${this.seconds < 0 ? 'backward' : 'forward'} ${amount} seconds`; } button.disabled = !this.#time.value; } #seek = () => { const time = this.#time.value; time?.seek(time.currentTime + this.seconds); }; } customElements.define(SeekByElement.tagName, SeekByElement); ``` --- # Media capability guards Type guards that narrow a Media object to the capabilities it supports The media a player attaches is typed as `Media`, which guarantees only a `play()` method and the `addEventListener`, `removeEventListener`, and `dispatchEvent` event methods. A native `