GuideMigrate
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/reactowns 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 add its source to your project 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:
The Minimal skin is the closest built-in starting point for this migration.
First, install the dependency:
Then create a reusable player component in your app:
Notes
@videojs/react/videois a preset: a player, a skin, and a media element that already fit together.VideoPlayeris 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, passrenderPosterto 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
originpoints somewhere other than your page’s origin, addcrossOrigin="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.cssand theMinimalVideoSkinimport and usage toVideoSkin.
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:
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:
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:
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:
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:
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:
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, add the skin source to your project and edit it. 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. Add the files for 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:
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:
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. Add the skin source to your project when you need deeper control over layout, control structure, icons, or interaction styling.
--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.
Edit skin source
For changes to controls, layout, or icons, add the skin source to your project. Its components and styles become local files. Customize skins covers the setup and available skins.
Known gaps
- Plyr’s
adsoption has no built-in equivalent. - Plyr’s
storageoption 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
sizemetadata 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 is not configurable yet (#1728). Disabling auto-hide is possible with
visibility="always"on the controls component, but not from a preset skin’s attributes. - Native controls are not automatically removed when custom controls load; see #1160.