Skip to content
FrameworkStyle

Migrate from Plyr

Move an existing Plyr integration to Video.js v10, mapping Plyr options and its instance API onto components and player state

Video.js v10 is not a drop-in replacement for Plyr. Plyr wraps one media element with a constructor and an options object; Video.js v10 is built a bit differently. The player is composed, not configured. That means the player you ship is smaller and faster, and when you’re ready to customize it, it’s easy to go under the hood and build what you need. It also means there’s a bit more set-up to get started.

The migration is mostly moving configuration into markup and replacing Plyr’s instance API with native media APIs or Video.js player state.

Why switch?

Plyr is simple and familiar, so the reason to migrate should be practical. These are the strongest wins when your app can use HTML5, HLS, DASH, Vimeo, YouTube, or Mux-backed media. We’ve taken our vast experience building some of the best players and thrown it all into Video.js v10.

  • ♿ Accessibility starts in the component model. Plyr has long-running reports around WCAG compliance, slider semantics, keyboard behavior, focus visibility, and menu roles (#905, #103). Video.js v10 exposes buttons, sliders, menus, radio groups, and tooltips as focused components with state attributes and accessibility behavior built into each part.
  • 📺 Streaming controls are first-class media state. Streaming was an afterthought in Plyr. HLS quality switching is one of Plyr’s most-requested gaps (#1741, #218). Video.js v10 models HLS, DASH, and Mux-backed media directly, so quality UI can read rendition state instead of parsing manifests and wiring hls.js in application code.
  • 🧩 Composition keeps player code cleaner. Players, media elements, skins, controls, gestures, and hotkeys are separate pieces. You can replace one part without wrapping or forking the whole player.
  • ✨ Two polished skins are ready to use. Minimal is the closest starting point for Plyr migrations, while Default gives you a modern, frosted design. Both can be ejected when you want to customize the layout, styling, or icons.
  • 📡 Remote playback is part of the built-in video UI. Plyr has an open Google Cast request dating back years (#112). Video.js v10 includes Cast and AirPlay controls through a shared remote playback feature.
  • ⚛️ React gets a native API. Plyr integrations in React tend to wrap an imperative constructor around framework-owned DOM (#254), and lifecycle cleanup can get awkward when routes or components unmount (#1001). @videojs/react gives you players, components, hooks, and TypeScript types instead.
  • 🎛️ Layout and skins are easier to own. Plyr’s iframe and aspect-ratio behavior has produced workarounds for cropping and fullscreen sizing (#339). Video.js v10 separates the player, container, media, and skin, so sizing belongs to the container or skin and the UI can be ejected when you need full control.
  • ⌨️ Input behavior is explicit. Plyr has recurring mobile tap and fullscreen threads (#718, #1190). Video.js v10 has common gestures and hotkey support built into the skins, and you can eject to customize them.

Basic migration

Let’s start with the most basic example of getting Plyr up and running and replace it with a Video.js v10 player.

This snippet will render a basic video player with thumbnail previews, captions and a poster image.

<link rel="stylesheet" href="path/to/plyr.css" />

<video id="player" src="/path/to/video.mp4" playsinline controls data-poster="/path/to/poster.jpg">
  <track kind="captions" label="English" src="/path/to/captions/en.vtt" srclang="en" default />
</video>

<script src="https://cdn.plyr.io/3.8.4/plyr.js"></script>

<script>
const player = new Plyr('#player', {
  previewThumbnails: {
    enabled: true,
    src: '/path/to/storyboard.vtt',
  },
});
</script>

The closest Video.js skin to the Plyr skin is the Minimal skin, so we’ll use that for this example.

First, install the dependency:

npm install @videojs/react

Then create a reusable player component in your app:

'use client';

import '@videojs/react/video/minimal-skin.css';
import { MinimalVideoSkin, Video, VideoPlayer } from '@videojs/react/video';

interface AppVideoPlayerProps {
  origin: string;
}

export function AppVideoPlayer({ origin }: AppVideoPlayerProps) {
  return (
    <VideoPlayer>
      <MinimalVideoSkin poster={`${origin}/poster.jpg`}>
        <Video src={`${origin}/video.mp4`} playsInline>
          <track kind="captions" label="English" src={`${origin}/captions/en.vtt`} srclang="en" default />
          <track kind="metadata" label="thumbnails" src={`${origin}/storyboard.vtt`} default />
        </Video>
      </MinimalVideoSkin>
    </VideoPlayer>
  );
}

Notes

  • @videojs/react/video is a preset: a player, a skin, and a media element that already fit together. VideoPlayer is the piece that owns the state, built from the standard set of features for video.
  • The poster is a prop on the skin rather than a poster attribute on the media, which gives the skin control over how it appears. That’s much like Plyr’s data-poster.
  • This example assumes all your files for a given asset live in the same path, using consistent filenames. This will almost certainly need adjusting for your implementation.
  • There’s also a default skin with a modern, frosted appearance that some may prefer. Try it by switching the CSS import filename to skin.css and the MinimalVideoSkin import and usage to VideoSkin.

Advanced migration

Streaming

If you’re using HLS or DASH to stream your media, you’re in luck; we have prebuilt media components you can slot in with much improved integration over Plyr implementations.

HLS

Replace <Video> with <HlsVideo> and add the import:

import { HlsVideo } from '@videojs/react/media/hls-video';

Set the src to the URL for the m3u8 manifest.

DASH

Very similar to HLS in that we have a drop-in component.

Replace <Video> with <DashVideo> and update your import:

import { DashVideo } from '@videojs/react/media/dash-video';

Set the src to the URL for the mpd manifest.

Vimeo

Vimeo is supported through a prebuilt component.

Replace <Video> with <VimeoVideo> and add the import:

import { VimeoVideo } from '@videojs/react/media/vimeo-video';

Set the src to the URL for the Vimeo video, for example https://vimeo.com/648359100.

YouTube

YouTube is supported through a first-class media component.

Replace <Video> with <YouTubeVideo> and add the import:

import { YouTubeVideo } from '@videojs/react/media/youtube-video';
<VideoPlayer>
  <MinimalVideoSkin>
    <YouTubeVideo src="https://youtu.be/aqz-KE-bpKQ" playsInline />
  </MinimalVideoSkin>
</VideoPlayer>

The component accepts YouTube watch, short, embed, Shorts, live, playlist, and privacy-enhanced URLs, as well as raw 11-character video IDs.

Internationalization

Video.js v10 ships with English labels by default and includes locale packs for:

ar, az, bg, bn, bs, ca, cs, cy, da, de, el, es, et, eu, fa, fi, fr, gd, gl, he, hi, hr, hu, id, it, ja, ko, lt, lv, mr, nb, ne, nl, nn, oc, pl, pt-BR, pt-PT, ro, ru, sk, sl, sr, sv, te, th, tr, uk, vi, zh-CN, and zh-TW.

The shorthand tags pt and zh are also available as aliases. See Internationalization for the full picture.

Use the React provider when you want scoped overrides:

'use client';

import '@videojs/react/video/minimal-skin.css';
import { I18nProvider } from '@videojs/react/i18n';
import { MinimalVideoSkin, Video, VideoPlayer } from '@videojs/react/video';

export function MyPlayer() {
  return (
    <VideoPlayer>
      <I18nProvider
        locale="en"
        translations={{
          buttons: {
            play: 'Start video',
            pause: 'Pause video',
          },
          menu: {
            settings: 'Options',
          },
        }}
      >
        <MinimalVideoSkin>
          <Video src="/video.mp4" playsInline />
        </MinimalVideoSkin>
      </I18nProvider>
    </VideoPlayer>
  );
}

Configuration

Plyr uses an object to set configuration options whereas Video.js v10 uses a component structure and attributes instead. We’re using a composition model rather than a configuration model. This reduces bundle size and only includes functionality you actually require.

Here’s a matrix for configuration options in Plyr and how each maps to Video.js v10:

Plyr option Video.js v10
controls The skins include all the common controls, laid out in a familiar way that users would expect. If you want to customize the skin beyond basic colors, you can eject the skin and change layout, styles, or icons.
settings Included automatically in the skins when quality, speed, audio tracks, or captions are available.
autoplay, muted, loop, playsinline, preload These are attributes on your media (e.g. <video>) component.
poster / data-poster The skin owns the poster, as shown in Basic migration above.
ratio Set aspect-ratio in CSS on the skin component.
hideControls Preset skins auto-hide controls based on activity. The delay is currently not configurable.
clickToPlay Preset video skins include click and tap gestures. Eject the skin to remove or change them.
keyboard Preset video skins include common hotkeys. Eject the skin to remove or change them.
tooltips Preset skins include tooltips for common controls. Eject the skin to customize them.
captions Add <track kind="captions"> or <track kind="subtitles">; preset skins show captions controls when tracks are available.
previewThumbnails Add <track kind="metadata" label="thumbnails">; preset video skins show slider thumbnails when thumbnail cues are available.
quality Works when the media provider exposes renditions. Plain MP4 source arrays do not currently become a quality menu automatically.
speed Included in the preset settings menu when playback rates are available.
fullscreen Native fullscreen is supported; Plyr’s full-window fallback is not a matching feature.
provider: 'vimeo' Use the Vimeo media component inside the player skin as shown in Vimeo above.
provider: 'youtube' Use the YouTube media component inside the player skin as shown in YouTube above.
storage Unsupported at this time.
i18n The most common languages are available by default but you can also provide custom translations, if required. See Internationalization above for more info.
ads Unsupported at this time.

Dynamic sources

In Video.js v10 you can swap out the src or the media component itself and the UI will update.

Imperative API

Use the media element for standard playback operations:

Plyr Video.js v10
player.play() video.play() or player action
player.pause() video.pause() or player action
player.currentTime = 10 video.currentTime = 10
player.volume = 0.5 video.volume = 0.5
player.muted = true video.muted = true
player.speed = 1.5 video.playbackRate = 1.5
player.fullscreen.enter() Fullscreen feature or fullscreen control
player.toggleCaptions() Text-track feature or captions control

For custom UI, read and write through the Video.js player store.

Import the preset’s usePlayer hook and call it directly or with a selector:

import { usePlayer } from '@videojs/react/video';

const currentTime = usePlayer((state) => state.currentTime);

Rewrite styles

Video.js v10 skins offer similar color customization via CSS custom properties. Eject the skin when you need deeper control over layout, control structure, icons, or interaction styling.

/* Plyr */
.plyr {
  --plyr-color-main: rebeccapurple;
}

/* Video.js */
video-player {
  --media-accent-color: rebeccapurple;
}

--media-accent-color reaches the sliders, active buttons, and accent surfaces, so it’s the closest match for Plyr’s --plyr-color-main. Video.js picks a readable text color to sit on top of it; set --media-accent-text-color to choose that yourself. --media-border-radius and --media-scale-unit cover rounding and control sizing. See Customize skins for the full list.

Ejecting

If you need extra customization of layout, styles, or icons, you can “eject” the skin into your application, much like a shadcn installation. Copy the skin and its CSS into your project, then modify the components and styles directly. Customize skins has copy-paste-ready implementations for every packaged skin.

Known gaps

  • Plyr’s ads option has no built-in equivalent.
  • Plyr’s storage option has no built-in equivalent for persisted volume, captions language, muted state, speed, or quality. Player setting persistence is tracked in #944; subtitle language preference is tracked in #1423.
  • Plyr’s full-window fullscreen fallback has no matching Video.js feature. This was designed as a fallback when the Fullscreen API wasn’t supported, but given browser support for fullscreen is around 96%, it’s unlikely to be required.
  • Plain MP4 source arrays with size metadata do not automatically create a quality menu. Use Mux, HLS, or DASH for adaptive quality when possible. A simpler source-driven quality menu may be considered later.
  • Preset skins segment the time slider and show chapter titles when the media includes a default <track kind="chapters">. Dedicated cue-point APIs are not complete yet; see #1442.
  • The controls auto-hide delay and disabled state are not configurable yet (#1728).
  • Native controls are not automatically removed when custom controls load; see #1160.