Aug 17, 2026•Breaking changes

# v10.0.0-beta.27

[Compare changes on GitHub](https://github.com/videojs/v10/compare/@videojs/core@10.0.0-beta.26...@videojs/core@10.0.0-beta.27)

Five new places to play from, DRM on every HLS path, and a long stabilization pass over the API you write against. Beta.27 is the largest release of the beta so far, and the one with the most to update.

## Breaking changes

There is a lot here. Read the list before you upgrade.

*   **Source and engine options live on `source`** ([#1903](https://github.com/videojs/v10/pull/1903), [#2014](https://github.com/videojs/v10/pull/2014)). The untyped `config` bag is retired. Everything describing a source sits on `source`, and engine settings are namespaced under `source.engine` by engine. `el.config = { preferPlayback: 'native', hlsJs: { maxBufferLength: 60 } }` becomes `el.source = { src, preferPlayback: 'native', engine: { hlsJs: { maxBufferLength: 60 } } }`. The keys are `hlsJs`, `nativeHls`, `dashJs`, `vimeo`, and `youtube`. `<dash-video>` gains working engine configuration for the first time.
*   **Mux derived URLs are read, not written** ([#1903](https://github.com/videojs/v10/pull/1903)). `source.thumbnail` is now `source.poster`, with a matching `poster-time` attribute on `<mux-video>`. The derived poster and storyboard URLs come from the read-only `contentData` record instead of `el.thumbnail` and `el.storyboard`.
*   **`SimpleHls*` is now `Hls*`** ([#2096](https://github.com/videojs/v10/pull/2096)). `<simple-hls-video>` becomes `<hls-video>`, `SimpleHlsVideo` becomes `HlsVideo`, and `SimpleHlsAudioOnly` becomes `HlsAudio`. Update the tag, the identifier, and the subpath: `@videojs/html/media/hls-video`, `@videojs/react/media/hls-audio`, `@videojs/spf/hls-video`. There is no compatibility shim. The `<hls-video>` tag was freed by the beta.25 rename of the hls.js element to `<hlsjs-video>`, which is unchanged.
*   **SPF background video is `hls-background-video`** ([#2097](https://github.com/videojs/v10/pull/2097), [#2062](https://github.com/videojs/v10/pull/2062)). The `@videojs/spf/background-video` entry point is gone; use `@videojs/spf/hls-background-video`. `BackgroundVideoMediaElement` is now `HlsBackgroundVideoMediaElement`, and its client-side `maxResolution` property is removed — narrow the rendition set with a delivery parameter on the URL (`?max_resolution=720p`) so the excluded renditions never reach the manifest. `<mux-background-video>` and `MuxBackgroundVideo` alias the new names. Neither background-video adapter declares `loop`, `muted`, `autoplay`, or `preload` any more; `attach` writes them onto the element instead.
*   **SPF Medias moved into `@videojs/spf`** ([#2033](https://github.com/videojs/v10/pull/2033)). `@videojs/media/dom/simple-hls` and `@videojs/media/dom/simple-hls-audio-only` stop resolving. Import the Medias from `@videojs/spf/hls-video` and `@videojs/spf/hls-audio`, and `createBackgroundVideoEngine` from `@videojs/spf/hls`. Engines share one entry per format family; adapters get one each.
*   **React `createPlayer()` returns `Player`** ([#2116](https://github.com/videojs/v10/pull/2116)). `const { Provider } = createPlayer(…)` becomes `const { Player } = createPlayer(…)`, and `ProviderProps` becomes `PlayerProps`. Presets now export their player directly, so most apps can skip `createPlayer` entirely.
*   **HTML `MediaElement` is now `UIElement`** ([#2245](https://github.com/videojs/v10/pull/2245)). Extend `UIElement` when creating player controls and other reactive HTML UI. Playback hosts such as `CustomMediaElement` are unchanged.
*   **Containers are off the `createPlayer()` result** ([#2154](https://github.com/videojs/v10/pull/2154)). Import `Container` from `@videojs/react` and `ContainerMixin` from `@videojs/html` instead of destructuring them.
*   **Menus lost their view machinery** ([#2029](https://github.com/videojs/v10/pull/2029)). Settings, i18n, value outlets, back-item types, and the viewport and view-stack abstractions are out of the base HTML and React menu APIs. Submenus are sibling nested menus opened by a trigger, back rows are ordinary items, and skins compose the captions, quality, playback rate, and audio track submenus themselves.
*   **Radio groups expose `options`** ([#2047](https://github.com/videojs/v10/pull/2047)). The quality, captions, playback rate, and audio track cores return one ordered `options` list — normalized values, labels, disabled state, and availability — in place of `tracks`, `rates`, and `renditions`. Quality’s Auto and captions’ Off are entries in that list rather than special cases beside it.
*   **Input indicators are separate components** ([#2046](https://github.com/videojs/v10/pull/2046)). The `inputFeedback` island and bubble styling contract is replaced by component-named ones: `statusIndicator`, `volumeIndicator`, `seekIndicator`, and the layout-only `inputIndicatorOverlay`. In authored CSS, `.media-input-feedback` becomes `.media-input-indicator-overlay`, `.media-input-feedback-island--volume` becomes `.media-volume-indicator`, and the bubble splits into `.media-status-indicator--playback` and `.media-seek-indicator`.
*   **Skin custom properties settled** ([#2094](https://github.com/videojs/v10/pull/2094)). `--media-color-primary` is now `--media-accent-color`, with `--media-accent-text-color` beside it as an escape hatch. The skin root is named `container` rather than `root`, dark mode runs through `light-dark()` so a parent’s `color-scheme` steers it, and `keyframes.css`, `properties.css`, and `tailwind.css` are gone. Skin-local variables drop the `--media-` prefix; that prefix now marks the public customization API.
*   **Time slider chapters need a template** ([#2204](https://github.com/videojs/v10/pull/2204)). Separate fallback children are gone. Supply an HTML `<template>` or a React `renderChapter`, and drop any duplicated fallback track. When there are no cues, core returns one cue-less range spanning the whole slider, so there is nothing left to fall back to.
*   **Some low-level positioning exports were removed** ([#1904](https://github.com/videojs/v10/pull/1904)). `getAnchorNameStyle`, `getPopoverCSSVars`, `PopoverCSSVarKey`, and `TooltipCSSVarKey` are gone from `@videojs/core/dom`, and `ManualOffsets` is now `PositioningOffsets`.

## More places to play from

YouTube, Cloudflare Stream, Spotify, TikTok, and Twitch all became media hosts this release ([#1853](https://github.com/videojs/v10/pull/1853), [#2168](https://github.com/videojs/v10/pull/2168), [#2169](https://github.com/videojs/v10/pull/2169), [#2170](https://github.com/videojs/v10/pull/2170), [#2171](https://github.com/videojs/v10/pull/2171)). Each is a port of its `muxinc/media-elements` counterpart, reshaped into the same `Media` host contract Vimeo already used, so `<youtube-video>` or `<TwitchVideo>` composes with the player, the store, and the skins like any other element. Embed parameters go under `source.engine`, typed and spelled the way each provider spells them.

They are not identical underneath, and the hosts say so rather than pretending. YouTube takes watch, `youtu.be`, `embed`, `shorts`, and `live` URLs, bare video ids, playlists, `t=1h30m15s` start times, and the `-nocookie` host, and derives `timeupdate` from a poll because the iframe API emits none. Spotify is audio-only with no volume, mute, or playback rate at all, so its rate feature stays off instead of showing a dead control. TikTok has no SDK — the `postMessage` protocol is the API. Twitch is the only one that plays live channels, and needs the framing hostnames its embed demands.

Two fixes make every embed better behaved. Vimeo and YouTube used to build their embed during `attach()`, which for a custom element runs in the constructor, before Svelte, Vue, or Angular has set `src`; the build is now deferred until a source resolves ([#2118](https://github.com/videojs/v10/pull/2118)). And clearing a source announces `emptied` on all six hosts, so the store stops reporting the previous video’s duration, buffered range, and stream type ([#2217](https://github.com/videojs/v10/pull/2217)).

Start from the media sources concept for [HTML](/docs/framework/html/concepts/media-sources/) and [React](/docs/framework/react/concepts/media-sources/).

## DRM and DASH quality

Protected content plays on every HLS path. `source.drm` names the license servers once, keyed by EME key system id, and the same object works on `NativeHlsVideo`, `HlsJsVideo`, `MuxVideo`, and `MuxAudio` ([#2014](https://github.com/videojs/v10/pull/2014)). hls.js hands it to its own `drmSystems` with EME switched on; for the browser’s native HLS, Video.js answers the element’s key requests itself. For Mux, a signed DRM token is the whole configuration — the FairPlay, Widevine, and PlayReady license servers are derived from the playback ID, and a token not scoped to DRM is ignored rather than sent ([#1948](https://github.com/videojs/v10/pull/1948)). Widevine asks for `HW_SECURE_ALL` first and falls back to whatever robustness the browser offers, so L1-restricted content plays where it can. See the elements for [HTML](/docs/framework/html/reference/mux-video/) and [React](/docs/framework/react/reference/mux-video/).

DASH picked up the standard video renditions API ([#2060](https://github.com/videojs/v10/pull/2060)). `videoRenditions` was always empty for `<dash-video>`, which left the quality menu unavailable; representations are now mirrored onto it, selecting one pins that representation and turns dash.js bitrate switching off, and clearing the selection hands switching back. Read the quality feature for [HTML](/docs/framework/html/reference/feature-quality/) and [React](/docs/framework/react/reference/feature-quality/), or the element for [HTML](/docs/framework/html/reference/dash-video/) and [React](/docs/framework/react/reference/dash-video/).

## Menus you compose yourself

The menu system got smaller ([#2029](https://github.com/videojs/v10/pull/2029)). Base menus now do generic menu behavior and nothing else: no coordinator, no viewport, no view stack, no settings controller. Submenus are sibling nested menus, opened by a trigger and closed by Escape, and each skin composes its own captions, quality, playback rate, and audio track panels from menu and radio group primitives. Transitions reuse the popover lifecycle rather than a separate machine. It’s a breaking change; see [Breaking changes](#breaking-changes).

Composing that way needs the primitives to exist in both frameworks, so React gained the three radio groups it was missing: audio track ([#2124](https://github.com/videojs/v10/pull/2124)), captions ([#2127](https://github.com/videojs/v10/pull/2127)), and quality ([#2132](https://github.com/videojs/v10/pull/2132)). Each is hook-backed with a required `renderItem`, so the group owns selection, labels, and ARIA while you own the markup. The shipped skins use them ([#2125](https://github.com/videojs/v10/pull/2125), [#2128](https://github.com/videojs/v10/pull/2128)), and all four are documented ([#2126](https://github.com/videojs/v10/pull/2126), [#2129](https://github.com/videojs/v10/pull/2129), [#2136](https://github.com/videojs/v10/pull/2136), [#2139](https://github.com/videojs/v10/pull/2139)): audio track for [HTML](/docs/framework/html/reference/audio-track-radio-group/) and [React](/docs/framework/react/reference/audio-track-radio-group/), captions for [HTML](/docs/framework/html/reference/captions-radio-group/) and [React](/docs/framework/react/reference/captions-radio-group/), quality for [HTML](/docs/framework/html/reference/quality-radio-group/) and [React](/docs/framework/react/reference/quality-radio-group/), and playback rate for [HTML](/docs/framework/html/reference/playback-rate-radio-group/) and [React](/docs/framework/react/reference/playback-rate-radio-group/).

Controls with nothing to offer now get out of the way. A radio group whose options are unavailable — one audio track, no captions, a single rendition — reports `hidden`, sets the native `hidden` attribute in HTML, and returns `null` in React ([#2069](https://github.com/videojs/v10/pull/2069)). The volume slider follows the same rule and refuses pointer, keyboard, and wheel input while volume control is unavailable ([#2072](https://github.com/videojs/v10/pull/2072)). Menu roving focus skips hidden items while keeping disabled-but-visible ones reachable ([#2071](https://github.com/videojs/v10/pull/2071)), and keyboard navigation no longer strands you when you back out of a submenu ([#2214](https://github.com/videojs/v10/pull/2214)). The E2E suite runs axe against WCAG A and AA on the loaded player and on open menus, and the React issues it found are fixed ([#2209](https://github.com/videojs/v10/pull/2209)). Details in the menu reference for [HTML](/docs/framework/html/reference/menu/) and [React](/docs/framework/react/reference/menu/), and the accessibility concept for [HTML](/docs/framework/html/concepts/accessibility/) and [React](/docs/framework/react/concepts/accessibility/).

## Chapters on the time slider

The chaptered time slider is rebuilt on independently rendered HTML and CSS segments instead of an SVG mask ([#2043](https://github.com/videojs/v10/pull/2043)), with normalized gaps, per-segment track, buffer and fill state, and chapter titles in the scrub preview. Default and Minimal render it in both the CSS and Tailwind sources. A follow-up removed the fallback concept entirely, so one template or `renderChapter` draws every range and a source with no cues gets a single full-width one ([#2204](https://github.com/videojs/v10/pull/2204)) — see [Breaking changes](#breaking-changes). The reference has the contract for [HTML](/docs/framework/html/reference/time-slider/) and [React](/docs/framework/react/reference/time-slider/).

## Skins, motion, and polish

The skins got a CSS clean-up that doubles as an API stabilization ([#2094](https://github.com/videojs/v10/pull/2094)): `--media-accent-color` and `--media-accent-text-color` replace `--media-color-primary`, `light-dark()` replaces the dark-mode media query, and three files disappeared. Motion across controls, menus, sliders, and status indicators was reworked to avoid remounting and discrete display changes, with hover motion limited to fine pointers and reduced-motion respected ([#2208](https://github.com/videojs/v10/pull/2208)). Submenu transitions ([#2130](https://github.com/videojs/v10/pull/2130), [#2159](https://github.com/videojs/v10/pull/2159)) and SVG icon animation ([#2216](https://github.com/videojs/v10/pull/2216)) were tuned alongside. Start from the skin customization guide for [HTML](/docs/framework/html/how-to/customize-skins/) and [React](/docs/framework/react/how-to/customize-skins/).

A batch of fixes rounds out the UI work:

*   Controls stay visible while a menu is open or a slider is being dragged, and the idle timer restarts when the interaction ends ([#1900](https://github.com/videojs/v10/pull/1900)).
*   The Settings button has its tooltip back, and it steps aside while the menu is open ([#1915](https://github.com/videojs/v10/pull/1915)).
*   Popups are steadier: default-open React popups measure a live trigger ([#1931](https://github.com/videojs/v10/pull/1931)), controls that share a menu and tooltip trigger keep the right CSS anchor ([#1933](https://github.com/videojs/v10/pull/1933)), and popup coordination is scoped to the visual container so a playlist or transcript beside the player no longer joins the skin’s popup group ([#2083](https://github.com/videojs/v10/pull/2083)).
*   Scrollable menus stop snapping back in Firefox when you move the pointer up an option ([#2100](https://github.com/videojs/v10/pull/2100)).
*   The live-edge dot stays red instead of being grayed out with the rest of the disabled live button ([#1921](https://github.com/videojs/v10/pull/1921)).
*   Ejected HTML players survive light DOM upgrading after the UI bundle runs, so volume and playback-rate popovers initialize without throwing ([#2031](https://github.com/videojs/v10/pull/2031)).
*   Cast and speech icons are flat paths rather than `mask: url(#a)`, which was risky with duplicate ids and is now also smaller ([#2027](https://github.com/videojs/v10/pull/2027)); the quality icon reads HD ([#2147](https://github.com/videojs/v10/pull/2147)).
*   Smaller passes on the default skin: a longer threshold before the time display hides ([#2030](https://github.com/videojs/v10/pull/2030)) and pixels as the default scale unit ([#2054](https://github.com/videojs/v10/pull/2054)).

## Preset-first React

`createPlayer()` used to be the front door. Now each preset exports its player directly — `VideoPlayer`, `AudioPlayer`, `LiveVideoPlayer`, `LiveAudioPlayer`, `BackgroundVideoPlayer` — each with its own typed `usePlayer`, and Next.js Server Components can render them ([#2116](https://github.com/videojs/v10/pull/2116)). `createPlayer()` is still there for custom feature compositions; React returns `Player`, while HTML returns `PlayerElement` rather than `ProviderMixin`. Layout primitives came off that result too: import `Container` from `@videojs/react` and `ContainerElement` from `@videojs/html` ([#2154](https://github.com/videojs/v10/pull/2154)). The guides, references, demos, and generated snippets were rewritten preset-first ([#2152](https://github.com/videojs/v10/pull/2152)). Both are breaking; see [Breaking changes](#breaking-changes). The references cover it for [HTML](/docs/framework/html/reference/html-create-player/) and [React](/docs/framework/react/reference/create-player/), with containers for [HTML](/docs/framework/html/reference/player-container/) and [React](/docs/framework/react/reference/player-container/).

A player title can also come from two places now. Feature state resolves user-supplied values against media-supplied ones, so a `content-title` attribute or `contentTitle` prop overrides what the media reports, while `defaultContentTitle` yields to it ([#1946](https://github.com/videojs/v10/pull/1946), [#1996](https://github.com/videojs/v10/pull/1996)).

## Playback fixes

hls.js took most of the attention. Pressing play with `preload="metadata"` while the first init segment was in flight put it into an unbounded request-and-abort loop that never started playback; raising the buffer limits no longer restarts the load ([#2103](https://github.com/videojs/v10/pull/2103)). A `<track>` child that loaded before hls.js got a source ended up selected and empty, because hls.js clears every text track’s cues on attach and no browser reparses a `<track>`; sideloaded cues and their mode now survive ([#2119](https://github.com/videojs/v10/pull/2119)). And a quality change no longer snaps the viewer’s audio language back to the manifest default when the audio group switches ([#2120](https://github.com/videojs/v10/pull/2120)).

Elsewhere: toggling captions on a source with several subtitle tracks turned all of them on at once, and now restores just the one you last had showing ([#2102](https://github.com/videojs/v10/pull/2102)). Mux Data assumed `media.engine` was an hls.js instance, so DASH was monitored through the wrong integration and lost every engine-level signal; engines are matched by shape and routed accordingly ([#2040](https://github.com/videojs/v10/pull/2040)). `@videojs/store` built an `AbortController` at module scope, which Cloudflare Workers and Deno Deploy disallow outside a request handler — it’s lazy now ([#2099](https://github.com/videojs/v10/pull/2099)). And `defaults()` enumerated the object it was given, which for every HTML UI element was the element itself: 342 enumerable WebIDL accessors read per update, forcing layout and subtree serialization each time. It reads the declared keys instead ([#2140](https://github.com/videojs/v10/pull/2140)).

## Self-hosting

CDN bundles no longer reach out to a public CDN at load. The i18n registry was statically imported from a hard-coded jsDelivr URL, which broke any mirrored copy of `cdn/`; the registry keeps its layers on `globalThis` and the bundles import it relatively, so a build resolves entirely within itself ([#2121](https://github.com/videojs/v10/pull/2121)). On top of that, every `@videojs/html` release now carries `videojs-html-<version>.zip` and `.tar.gz` with a `SHA256SUMS` file — roughly 700 KB of production bundles, no sourcemaps — which gives Composer-based projects like Drupal a browser-ready player again ([#2122](https://github.com/videojs/v10/pull/2122)). The release run verifies the bundles are self-contained before publishing. See the self-hosting guide for [HTML](/docs/framework/html/how-to/self-host-the-player/) and [React](/docs/framework/react/how-to/self-host-the-player/).

## Under the hood: SPF

Live HLS re-landed on the presentation-timeline model ([#1884](https://github.com/videojs/v10/pull/1884)). A per-source anchor frozen from `EXT-X-PROGRAM-DATE-TIME` settles segment placement at parse time, live windows re-place from PDT on every reload, and `seekable` is the intersection of the selected audio and video windows mirrored into `MediaSource.setLiveSeekableRange`. Playback starts at the live edge minus HOLD-BACK. Scope is sliding-window live consumed through the live skin; DVR and LL-HLS are not in it.

A source the engine can’t play now fails visibly instead of stalling in silence: producers append SVTA-coded conditions, the adapter decides which are fatal, and core maps the surfaced code to localized copy ([#1936](https://github.com/videojs/v10/pull/1936)).

`<mux-video>` and `<mux-audio>` can run on SPF instead of hls.js, chosen by import path — `@videojs/html/media/mux-video/spf` sits beside the hls.js flavor, with the same element surface and the same React components ([#2045](https://github.com/videojs/v10/pull/2045)). The SPF `<mux-audio>` fetches only audio renditions, where the hls.js flavor downloads video it never plays. The packaging behind all this moved: SPF Medias live in `@videojs/spf` rather than `@videojs/media` ([#2033](https://github.com/videojs/v10/pull/2033)), `<mux-background-video>` landed over the SPF background-video engine ([#2062](https://github.com/videojs/v10/pull/2062)), and the two identical background adapters collapsed into one named `hls-background-video` ([#2097](https://github.com/videojs/v10/pull/2097)). All three are breaking; see [Breaking changes](#breaking-changes).

## Skins from source

Work started on generating the shipped skins from one canonical source. A private compiler package handles parsing, import rewriting, JSX transforms, and deterministic multi-entry output ([#1981](https://github.com/videojs/v10/pull/1981), [#1990](https://github.com/videojs/v10/pull/1990)); core gained a constrained, target-neutral JSX runtime that rejects platform intrinsics ([#1986](https://github.com/videojs/v10/pull/1986), [#1995](https://github.com/videojs/v10/pull/1995)); and `@videojs/skins` gained a canonical source tree beside its current build inputs ([#1989](https://github.com/videojs/v10/pull/1989)). The canonical components landed piece by piece — play button, default video controls, volume controls, container, poster, and overlay ([#1991](https://github.com/videojs/v10/pull/1991), [#1992](https://github.com/videojs/v10/pull/1992), [#1993](https://github.com/videojs/v10/pull/1993), [#2179](https://github.com/videojs/v10/pull/2179)) — along with Tailwind styles, HTML and React compiler plugins, a registry catalog adapter, and the pipeline that projects all of it into readable React, HTML, scoped CSS, and a shadcn-compatible registry ([#2005](https://github.com/videojs/v10/pull/2005), [#2006](https://github.com/videojs/v10/pull/2006), [#2007](https://github.com/videojs/v10/pull/2007), [#2008](https://github.com/videojs/v10/pull/2008), [#2021](https://github.com/videojs/v10/pull/2021)). Nothing here changes what the packages ship yet — the generated output is staged until parity is done.

## Docs and i18n

Lithuanian joined the built-in locale packs, hand-written against the English source rather than ported from v8, which had no Lithuanian at all ([#1917](https://github.com/videojs/v10/pull/1917)). Translations elsewhere got a pass ([#1914](https://github.com/videojs/v10/pull/1914)), the elapsed and remaining time labels read better ([#2028](https://github.com/videojs/v10/pull/2028)), and the Hindi quality menu uses resolution wording ([#2098](https://github.com/videojs/v10/pull/2098)). Read the i18n guide for [HTML](/docs/framework/html/how-to/internationalize-the-player/) and [React](/docs/framework/react/how-to/internationalize-the-player/).

On the site, the API reference is grouped under one heading ([#2023](https://github.com/videojs/v10/pull/2023)), the menu composition demos match the simplified model ([#2131](https://github.com/videojs/v10/pull/2131)), and the time slider reference and HTML sub-part props were repaired ([#2142](https://github.com/videojs/v10/pull/2142), [#2146](https://github.com/videojs/v10/pull/2146)). Draft migration guides for Plyr ([#1740](https://github.com/videojs/v10/pull/1740)) and Media Chrome ([#1706](https://github.com/videojs/v10/pull/1706)) landed in the repository, pending verification before they reach the site.

Thanks to [@littlespex](https://github.com/littlespex) for their first contribution to Video.js.

---

All documentation: https://videojs.org/llms.txt