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 options object. Video.js composes a player, media component, and skin in markup, then exposes behavior through player state.

Start with the Minimal skin, move media settings into markup, and replace calls to the Plyr instance with native media APIs or Video.js state actions.

What changes

  • The player becomes three pieces. Player state, the media component, and the skin are separate, so you can replace one without wrapping or forking the others.
  • Streaming behavior becomes player state. HLS, DASH, and Mux integrations expose renditions, tracks, and live state to the UI instead of leaving that wiring to application code.
  • React uses native components and hooks. @videojs/react owns its lifecycle instead of wrapping an imperative constructor around framework-owned DOM.
  • The UI is composable. Buttons, sliders, menus, gestures, and hotkeys are individual accessible components. Start with the Minimal or Default skin, then eject it when you need to change the structure.
  • Remote playback is built in. The video skins include AirPlay and Cast controls. Follow Cast to AirPlay and Chromecast to add the Google Cast extension.

Basic migration

Start with a Plyr player that has captions, thumbnail previews, and a poster:

<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 Minimal skin is the closest built-in starting point for this migration.

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 poster={`${origin}/poster.jpg`}>
      <MinimalVideoSkin className="app-video-player">
        <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 URL is metadata on VideoPlayer. The skin reads that metadata and controls how the poster appears. To replace the rendered image, pass renderPoster to the skin — see Add a poster and loading placeholder.
  • 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.
  • If origin points somewhere other than your page’s origin, add crossOrigin="anonymous" to <Video> and serve those files with CORS headers. A cross-origin thumbnail track only loads when the media element is CORS-enabled. See Thumbnail.
  • 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.

Map common features

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:

npm install @videojs/react @videojs/spf
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:

npm install @videojs/react @videojs/dash-video
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:

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

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

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 Internationalize the player 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 the common controls, laid out in a familiar way that users would expect. Not every Plyr control ships in every skin—the video skins do not include rewind and fast-forward buttons, for example. To change which controls appear, or to customize the skin beyond basic colors, eject the skin and change controls, layout, styles, or icons.
rewind, fast-forward, seekTime The audio skins include 10-second skip buttons; the video skins do not. Eject the skin and add a SeekButton, which skips by its seconds value (default 30; negative values seek backward).
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 Set poster on <video-player> or VideoPlayer. See Basic migration.
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.

Customize the controls

Plyr’s controls option chooses which controls appear. Video.js skins come with their own control set and layout. Keep the skin when that UI fits your player. To remove, reorder, or restyle its controls, eject the skin and edit the copied implementation. Adding a child to a skin does not place it inside the control bar.

If your app already has a custom control bar, build it from individual UI components. Video.js handles the media action, accessible name, and state. You add the visible contents, layout, and CSS.

Individual React buttons do not include visible content. Use their render props to add an icon or text and style the element you return. Eject a ready-made skin when you need to change its control set or layout.

Use the imperative API

Control everything through the player’s store. Every Plyr call has a matching store action, so one mental model covers playback, volume, fullscreen, and captions alike:

Plyr Video.js v10 store action
player.play(), player.pause() play(), pause(), or togglePaused()
player.currentTime = 10 seek(10)
player.volume = 0.5 setVolume(0.5)
player.muted = true toggleMuted()
player.speed = 1.5 setPlaybackRate(1.5)
player.fullscreen.enter() requestFullscreen()
player.toggleCaptions() toggleSubtitles()

You can still script the media element directly when you want to. Plyr routed calls through its wrapper because the wrapper had to know about every change; Video.js derives player state from the native media events, so video.play() or video.currentTime = 10 keeps every control in sync. The media element is also the way to change content: set src on the media component to swap sources, and replace the component when the media type changes, such as moving from Video to HlsVideo. The player UI follows the attached media.

Import the preset’s usePlayer hook and call it from a descendant of VideoPlayer. The component that creates VideoPlayer cannot also consume its context, so put store access in a child component:

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

function CurrentTime() {
  const currentTime = usePlayer((state) => state.currentTime);
  return <output>{Math.round(currentTime)} seconds</output>;
}

export function AppVideoPlayer() {
  return (
    <VideoPlayer>
      <MinimalVideoSkin>
        <Video src="/video.mp4" playsInline />
        <CurrentTime />
      </MinimalVideoSkin>
    </VideoPlayer>
  );
}

The same hook selects actions: usePlayer((state) => state.togglePaused) returns a function you can call from your own UI.

When you need the element itself, put a ref on the media component; its value is the rendered HTMLVideoElement, so ref.current.play() works the way Plyr’s underlying element did. For the Video.js media object, call useMedia from a component inside the player; media that wrap a playback engine expose it there through the engine escape hatch.

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 React: the className from the basic example */
.app-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.

Eject a skin

For changes to controls, layout, or icons, eject the skin into your project. Its components and styles become files you own. Customize skins covers the setup and available skins.

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.