# plyr: a vanilla JavaScript player for HTML5, YouTube and Vimeo

> Plyr wraps native media elements and iframe embeds behind one API, one event set and one stylesheet. It is a good fit when you want consistent controls without a framework, and a poor fit when you need DRM or a full streaming stack.

**sampotts/plyr** — A simple HTML5, YouTube and Vimeo player

- Repository: https://github.com/sampotts/plyr
- Website: https://plyr.io
- Stars: 30,015 · Forks: 3,118
- Language: JavaScript
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/sampotts-plyr

## The problem Plyr solves: three media sources, one control surface

A page that mixes a self-hosted MP4 with a YouTube embed ends up with two different players, two sets of keyboard shortcuts and two event models. Plyr's answer is to keep the markup you already have and take over the controls. For HTML5 it extends the standard media element markup; for YouTube and Vimeo it uses progressive enhancement on the default iframe embeds. The README describes the result as a simple, lightweight, accessible and customizable player for HTML5, YouTube and Vimeo that supports modern browsers.

The audience is narrow but real. Someone embedding a product demo on a marketing page. A documentation site with screencasts. A course platform that hosts MP4s on its own storage but also embeds Vimeo for legacy content. These are pages where a full streaming player is overkill, but the browser default controls look different in every browser and give you no consistent API. Plyr is written in vanilla ES6 JavaScript with no jQuery, and the stylesheet is Sass, so it drops into a build pipeline rather than demanding a runtime framework.

## How Plyr works: native elements, iframe enhancement and a normalized event layer

For HTML5 media there is no wrapper abstraction to learn. The README states that Plyr extends upon the standard HTML5 media element markup, so a video element with source children is the input. The library then builds its own control layer on top. The README makes a point of the element choices: input type range for volume, progress for progress, and button elements for buttons, rather than span or anchor hacks. That matters for screen readers and for form semantics, and it is the reason the accessibility claim is not just a label.

For YouTube and Vimeo the flow runs the other way. Plyr progressively enhances the default iframe embed. You can hand it an iframe inside a container with the plyr__video-embed class, or skip the iframe entirely and use a div with data-plyr-provider and data-plyr-embed-id. In both cases Plyr talks to the provider's own API and re-emits the results as its own events. The README frames this as no messing around with Vimeo and YouTube APIs, with all events standardized across formats. That normalization is the actual product: one place to listen for play, pause and time updates regardless of where the bytes come from.

Streaming is delegated rather than implemented. The feature list names hls.js, Shaka and dash.js support, and the demos link to Codepen templates for each. Plyr supplies the controls and the API surface; the streaming library does the manifest parsing and segment loading. That division is worth understanding before you evaluate it, because it means Plyr inherits the limitations of whichever library you pair it with.

## Installing Plyr and wiring up a first player

The package is published to npm as plyr, and the package.json exports map points importers at dist/plyr.mjs for ESM, dist/plyr.js for CommonJS and dist/plyr.min.js for the browser field. The stylesheet is exported separately as dist/plyr.css, and the raw Sass source is exposed at src/sass/plyr.scss for projects that compile Sass themselves.

Install it with your package manager of choice:

```bash
pnpm add plyr
```

Then import the module and the stylesheet. The README's ES6 example constructs the player from a CSS selector:

```js
import Plyr from 'plyr';
import 'plyr/dist/plyr.css';

const player = new Plyr('#player');
```

The selector must match an element in the page. For HTML5 video, that element is the video tag itself, and the README recommends data-poster rather than the poster attribute to avoid the image being downloaded twice:

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

If you are not using a bundler, the README offers a Cloudflare-hosted CDN build and a polyfilled variant of it:

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

For YouTube without an iframe, the non-progressive-enhancement path is a div carrying two data attributes. The README notes that data-plyr-embed-id accepts either the video ID or a full URL:

```html
<div id="player" data-plyr-provider="youtube" data-plyr-embed-id="bTqVqk7FSmY"></div>
```

After the script runs, the element you selected should be replaced by Plyr's control markup and the native controls should be gone. If nothing changes, the usual cause is that the script executed before the target element existed, or the selector matched nothing.

## Where Plyr stops: DRM, ads and the cost of delegating streaming

Plyr is not a streaming player and does not pretend to be one. HLS, DASH and Shaka playback arrive through those separate libraries, so anything those libraries cannot do, Plyr cannot do either. If your content is protected by Widevine or FairPlay, the feature list does not mention DRM at all, and you should assume the decision has to be made at the streaming library layer, not here.

Browser support is explicitly scoped to modern browsers. The package.json sets browserslist to "> 1%", and the README links to a browser support section rather than promising broad legacy coverage. The polyfilled CDN build exists for a reason: core-js, custom-event-polyfill, loadjs, rangetouch and url-polyfill are runtime dependencies, and rangetouch in particular exists to make range inputs behave on touch devices. If your audience includes old Android WebViews, that is the build to evaluate, and the README's own recommendation is to manage polyfills separately as part of your application.

There is also a markup constraint that catches people out. The poster image should be set with data-poster, not the poster attribute, to prevent it being downloaded twice. The README allows the poster attribute when you are sure the image will be cached, but the default advice is data-poster. Captions must be VTT tracks. There is no subtitle conversion step in the library, so SRT files need converting before they reach the page.

## Plyr compared with Video.js and the native controls

The closest general-purpose alternative is Video.js. The difference is architectural. Video.js is a component framework: it defines its own player class, its own component tree, its own plugin registry, and skins are built by extending that tree. Plyr does the opposite. It takes the elements the browser already gives you and styles them, and for embeds it enhances the provider's iframe rather than replacing it. The practical consequence is that Plyr has far less surface area to learn and far less to configure, while Video.js has more room for plugins that hook deep into playback.

A second alternative is doing nothing: use the native controls with the controls attribute and skip the library. That is genuinely reasonable for a single video on a page. The trade-off is that native controls differ across browsers, give you no consistent JavaScript API, and offer no unified event names when you also embed YouTube. Plyr's value appears the moment you have more than one media source on a site, or you need to drive playback from your own UI.

A third path is building the controls yourself against the HTMLMediaElement API. That is more work than it sounds once you account for keyboard shortcuts, fullscreen fallbacks, picture-in-picture, captions menus and the touch behaviour of range inputs. Plyr's dependency list is short and its stylesheet is Sass, so the cost of adopting it is closer to the cost of a small internal component than to the cost of a platform.

## Maintenance, licence and what upgrading actually costs

The repository is not archived, and the last push was on 2026-09-10. The most recent release listed is v3.8.4 on 2026-01-03, following v3.8.3 and v3.8.2 on 2025-08-27. That is a slow cadence: roughly two release clusters in the period covered, with the version number moving in patch increments. The package.json version matches the release tag, and the README's CDN example pins the same version, so the documentation is kept in step with the published build.

The licence is MIT, declared in package.json and present as LICENSE.md at the repository root. For most teams that means the usual permissive terms apply: keep the copyright notice and the licence text with distributions. It does not answer questions about the media you play through it, the provider terms for YouTube and Vimeo embeds, or the licences of the streaming libraries you pair it with. Those are separate evaluations, and nothing in this repository resolves them.

Upgrade cost is low but not zero. Because the API is a constructor plus options plus events, most upgrades are a version bump in package.json and a matching change to the CDN URL if you use one. The larger migration risk sits in the stylesheet: if you have overridden Plyr's Sass variables or targeted its class names, a rebuild can shift selectors. The exports map also matters for bundler users, since the import path and the CSS path are declared separately and a misconfigured resolver can pick the wrong entry.

## Conclusion

Adopt Plyr if you already ship plain HTML media elements or YouTube and Vimeo iframes and want one control surface, one event set and VTT captions without pulling in a framework. Skip it if you need DRM, server-side ad insertion or a player that owns the streaming stack, since Plyr delegates HLS, DASH and Shaka playback to those libraries. Before committing, check the browser support table against your own analytics, confirm that your caption files are VTT, and decide whether the polyfilled build or your existing polyfill pipeline is the one you ship.

## FAQ

### What is HTML5 video?

It is the browser's built-in video element and its JavaScript API, which Plyr extends rather than replaces. In Plyr's case the markup is the standard video tag with source children, and Plyr adds its own control layer on top.

### What is the HTML code to play a video?

Plyr uses the standard media element markup, so a video tag with one or more source children and an optional track element for captions. The README recommends setting the poster image with data-poster instead of the poster attribute to avoid downloading it twice.

### What is a Vimeo player?

It is Vimeo's embedded iframe player, which Plyr progressively enhances. The README shows wrapping the iframe in a container with the plyr__video-embed class, or using a div with data-plyr-provider set to vimeo and data-plyr-embed-id set to the video ID or URL.

## Sources

- [License: MIT](https://github.com/sampotts/plyr/blob/master/LICENSE)
- [Project website](https://plyr.io)
- [README](https://github.com/sampotts/plyr/blob/master/README.md)
- [Releases](https://github.com/sampotts/plyr/releases)
- [sampotts/plyr on GitHub](https://github.com/sampotts/plyr)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sampotts-plyr
