# 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](https://videojs.org/docs/framework/react/guides/features).

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 [add its source to your project](#edit-skin-source) 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](https://videojs.org/docs/framework/react/guides/casting) to add the Google Cast extension.

> **Note**
>
> Video.js v10 is still moving toward GA. Features like ads, playlists, preference persistence, and cue-point APIs are active areas, so check [Known gaps](#known-gaps) when those features matter to your migration.

## Basic migration

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

```html
<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:

```bash
npm install @videojs/react
```

Then create a reusable player component in your app:

```tsx
'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](https://videojs.org/docs/framework/react/guides/presets): 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](https://videojs.org/docs/framework/react/guides/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](https://videojs.org/docs/framework/react/guides/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.
- 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](https://videojs.org/docs/framework/react/reference/components/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](https://videojs.org/docs/framework/react/guides/media-sources) you can slot in with much improved integration over Plyr implementations.

#### HLS

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

```bash
npm install @videojs/react @videojs/spf
```

```js
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:

```bash
npm install @videojs/react @videojs/dash-video
```

```js
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:

```bash
npm install @videojs/react @videojs/vimeo-video
```

```js
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:

```js
import { YouTubeVideo } from '@videojs/react/media/youtube-video';
```

```jsx
<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](https://videojs.org/docs/framework/react/guides/internationalization) for the full picture.

Use the React provider when you want scoped overrides:

```tsx
'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](https://videojs.org/docs/framework/react/guides/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, [add the skin source to your project](#edit-skin-source) and change controls, layout, styles, or icons. |
| `rewind`, `fast-forward`, `seekTime` | The audio skins include 10-second skip buttons; the video skins do not. [Add the skin source to your project](#edit-skin-source), then add a [seek button](https://videojs.org/docs/framework/react/reference/components/seek-button), 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 the player. See [Basic migration](#basic-migration). |
| `ratio` | Set `aspect-ratio` in CSS on the skin component. |
| `hideControls` | Preset skins auto-hide controls based on activity. `hideControls: false` maps to `visibility="always"` on the controls component in a custom or installed skin layout; the delay is currently not configurable. |
| `clickToPlay` | Preset video skins include click and tap gestures. [Add the skin source to your project](#edit-skin-source) to remove or change them. |
| `keyboard` | Preset video skins include common hotkeys. [Add the skin source to your project](#edit-skin-source) to remove or change them. |
| `tooltips` | Preset skins include tooltips for common controls. [Add the skin source to your project](#edit-skin-source) 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](#vimeo) above. |
| `provider: 'youtube'` | Use the YouTube media component inside the player skin as shown in [YouTube](#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](#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, [add the skin source to your project](#edit-skin-source) 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](https://videojs.org/docs/framework/react/guides/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:

| 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`](https://videojs.org/docs/framework/react/reference/api/use-player) 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:

```tsx
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. [Add the skin source to your project](#edit-skin-source) when you need deeper control over layout, control structure, icons, or interaction styling.

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

```css
/* 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](https://videojs.org/docs/framework/react/guides/customize-skins#style-a-packaged-skin) 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](https://videojs.org/docs/framework/react/guides/customize-skins#style-skin-source) 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](https://github.com/videojs/v10/issues/944); subtitle language preference is tracked in [#1423](https://github.com/videojs/v10/issues/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%](https://caniuse.com/fullscreen), 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](https://github.com/videojs/v10/issues/1442).
- The controls auto-hide delay is not configurable yet ([#1728](https://github.com/videojs/v10/issues/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](https://github.com/videojs/v10/issues/1160).

## Related pages

### Guides

- [Features](https://videojs.org/docs/framework/react/guides/features): The state and actions each feature adds to the player
- [Media sources](https://videojs.org/docs/framework/react/guides/media-sources): Set what a media element plays and how its engine plays it with the structured source property
- [Customize skins](https://videojs.org/docs/framework/react/guides/customize-skins): Style a packaged Video.js skin or add its source to change controls, layout, styles, and interactions

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
