npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elementmints/shadow-plyr

v1.8.1

Published

A fully customizable, responsive web component for video playback with advanced features.

Readme

Shadow Plyr

npm version npm downloads bundle size license


🎬 Demo

<shadow-plyr show-controls="true" show-center-play="true">
  <source src="https://example.com/video.mp4" type="video/mp4">
</shadow-plyr>

✨ Features

| Feature | Supported | |---|---| | Web Component / Shadow DOM | ✅ | | Adaptive HLS streaming | ✅ | | Quality selector | ✅ | | Subtitles / captions | ✅ | | Unified settings menu (quality + speed + subtitles) | ✅ New | | Netflix-style seek thumbnail preview | ✅ New | | Custom loader (SVG / image / GIF) | ✅ New | | UI error overlay with retry | ✅ New | | Poster shown under loader during buffering | ✅ New | | Mobile gestures | ✅ | | Theater mode | ✅ | | Mini player | ✅ | | Picture-in-Picture | ✅ | | Screenshot capture | ✅ | | AirPlay support | ✅ | | Custom SVG icons | ✅ | | CSS theming via ::part | ✅ | | Lazy loading | ✅ | | Resume playback | ✅ | | Keyboard shortcuts | ✅ |


⚡ Quick Start

Install

npm install @elementmints/shadow-plyr
import "@elementmints/shadow-plyr";

Or use CDN

<script type="module"
  src="https://unpkg.com/@elementmints/shadow-plyr@latest/dist/index.js">
</script>

🚀 Basic Usage

<!-- Preferred: use <source> child elements -->
<shadow-plyr show-controls="true" show-center-play="true">
  <source src="https://example.com/video.mp4" type="video/mp4">
</shadow-plyr>

🖼 Poster / Thumbnail

Use a <picture> child element for full art-direction support. The poster image is shown during loading at 75 % opacity underneath the loader, and disappears once playback starts.

<shadow-plyr show-controls="true">
  <picture>
    <source media="(max-width: 768px)" srcset="poster-mobile.jpg">
    <img src="poster-desktop.jpg" alt="Video thumbnail">
  </picture>
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

If no poster is supplied, the player shows a plain black background with the loader centred on top.


📼 Video Sources

Supply video with <source> child elements. Multiple sources enable quality switching.

<shadow-plyr show-controls="true" show-quality="true">
  <source src="video-1080p.mp4" type="video/mp4" data-quality="1080">
  <source src="video-720p.mp4"  type="video/mp4" data-quality="720">
  <source src="video-480p.mp4"  type="video/mp4" data-quality="480">
</shadow-plyr>

data-quality labels appear in the quality menu (e.g. "1080" → shown as 1080p).


⚙️ Unified Settings Menu (New in v1.7)

Combine Quality, Speed, and Subtitles into a single gear-icon menu with a back-navigable submenu. Ideal for small screens.

<shadow-plyr
  show-controls="true"
  show-settings="true"
  show-quality="true"
  show-speed="true"
  show-subtitles="true">
  <source src="video.mp4" type="video/mp4" data-quality="1080">
</shadow-plyr>

When show-settings="true", the individual show-quality, show-speed, and show-subtitles buttons are hidden from the controls bar and merged into the settings panel. The individual pop-up menus still work if show-settings is omitted.


🖼 Seek Thumbnail Preview (New in v1.7)

Show a thumbnail when the user hovers over or drags the seekbar — Netflix-style.

Option A — WebVTT sprite sheet (recommended)

<shadow-plyr
  show-controls="true"
  show-seekbar="true"
  show-thumbnails="true"
  thumbnails-vtt="https://example.com/thumbnails.vtt">
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

A WebVTT file maps time ranges to image coordinates:

WEBVTT

00:00:00.000 --> 00:00:05.000
thumbnails.jpg#xywh=0,0,160,90

00:00:05.000 --> 00:00:10.000
thumbnails.jpg#xywh=160,0,160,90

Option B — Live frame capture (no extra files)

<shadow-plyr
  show-controls="true"
  show-seekbar="true"
  show-thumbnails="true">
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

When thumbnails-vtt is omitted a hidden <video> clone scrubs to the hovered time and draws the frame onto an off-screen canvas.

| Attribute | Type | Default | Description | |---|---|---|---| | show-thumbnails | boolean | false | Enable the seek thumbnail preview | | thumbnails-vtt | string | "" | URL to a WebVTT thumbnail track |


❌ Error Handling (New in v1.7)

When a video fails to load, the player now shows a styled error overlay inside the video container instead of silently logging to the console.

<shadow-plyr
  show-controls="true"
  error-message="The video could not be loaded. Please try again."
  show-retry="true">
  <source src="https://example.com/video.mp4" type="video/mp4">
</shadow-plyr>

Supply a custom icon (sanitized SVG):

<shadow-plyr
  error-message="Oops! Something went wrong."
  error-icon='<svg viewBox="0 0 24 24"><path d="M12 2 1 21h22L12 2zm1 15h-2v-2h2v2zm0-4h-2V9h2v4z"/></svg>'>
</shadow-plyr>

| Attribute | Type | Default | Description | |---|---|---|---| | error-message | string | "An error occurred while loading the video." | Message shown in the error overlay | | error-icon | string | Built-in triangle icon | Custom SVG icon (sanitized via DOMPurify) | | show-retry | boolean | true | Show a Try again button |

The error overlay is also accessible via ::part(error-overlay).


⏳ Custom Loader (New in v1.7)

Replace the default CSS spinner with any image, animated GIF, or inline SVG.

Image / GIF

<shadow-plyr loader-src="https://example.com/spinner.gif">
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

Inline SVG / HTML

<shadow-plyr loader-html='<svg viewBox="0 0 50 50" class="my-spin">…</svg>'>
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

The HTML is sanitized with DOMPurify (SVG profile) before insertion.

| Attribute | Type | Default | Description | |---|---|---|---| | loader-src | string | "" | URL to an image or GIF (HTTPS only) | | loader-html | string | "" | Raw HTML/SVG markup for the loader |

When either attribute is set, the default CSS spinner is hidden automatically.


📺 Subtitles

<shadow-plyr show-controls="true" show-subtitles="true">
  <track src="subtitles-en.vtt" kind="subtitles" srclang="en" label="English">
  <track src="subtitles-es.vtt" kind="subtitles" srclang="es" label="Español">
  <source src="video.mp4" type="video/mp4">
</shadow-plyr>

🎛 Controls Reference

<shadow-plyr
  show-controls="true"
  show-play-pause="true"
  show-seekbar="true"
  show-volume="true"
  show-fullscreen="true"
  show-speed="true"
  show-quality="true"
  show-subtitles="true"
  show-settings="true"
  show-loop="true"
  show-pip="true"
  mini-player="true"
  theater-mode="true"
  screenshot="true"
  airplay="true"
  responsive-controls="true">
</shadow-plyr>

👆 Gestures

<shadow-plyr
  double-tap-seek="true"
  double-tap-seek-seconds="10"
  triple-tap-seek="true"
  triple-tap-seconds="30"
  enable-tap-ripple="true">
</shadow-plyr>

| Gesture | Action | |---|---| | Double tap left | −N seconds | | Double tap right | +N seconds | | Triple tap left | −30 seconds | | Triple tap right | +30 seconds | | Single tap | Play / Pause | | Seekbar drag | Scrub (shows thumbnail preview when enabled) |


🧠 Performance & Behaviour

<shadow-plyr
  lazy="true"
  pause-on-out-of-view="true"
  pause-on-tab-hide="true"
  single-active="true"
  resume="true">
</shadow-plyr>

| Attribute | Default | Description | |---|---|---| | lazy | false | Load video only when near the viewport | | pause-on-out-of-view | false | Pause when scrolled off-screen | | pause-on-tab-hide | true | Pause when the browser tab is hidden | | single-active | false | Only one player plays at a time per page | | resume | false | Resume from last saved position (uses localStorage) |


🎨 Theming

CSS custom properties

shadow-plyr {
  --accent-color:    #00ffcc;
  --controls-bg:     rgba(0,0,0,0.95);
  --tooltip-bg:      #333;
  --tooltip-color:   #fff;
  --center-play-bg:  rgba(0,0,0,0.7);
  --center-play-size: 80px;
  --aspect-ratio:    16/9;
}

HTML attributes

<shadow-plyr
  theme="dark"
  accent-color="#ff3b30"
  controls-background="rgba(0,0,0,0.9)"
  center-play-background="rgba(0,0,0,0.7)"
  center-play-size="90">
</shadow-plyr>

::part CSS API

Every major element exposes a CSS part:

shadow-plyr::part(controls)      { background: rgba(0,0,0,0.9); }
shadow-plyr::part(play-pause)    { border-radius: 50%; }
shadow-plyr::part(seekbar)       { height: 6px; }
shadow-plyr::part(seekbar-progress) { background: hotpink; }
shadow-plyr::part(center-play)   { background: rgba(255,255,255,0.2); }
shadow-plyr::part(time-display)  { font-family: monospace; }
shadow-plyr::part(error-overlay) { background: rgba(0,0,0,0.95); }
shadow-plyr::part(custom-loader) { /* custom loader wrapper */ }

🧩 Custom Icons

All icons accept raw SVG strings and are sanitized with DOMPurify:

<shadow-plyr
  play-icon='<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>'
  pause-icon='<svg viewBox="0 0 24 24"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg>'
  fullscreen-icon='…'
  exit-fullscreen-icon='…'
  volume-icon='…'
  muted-icon='…'
  speed-icon='…'
  loop-icon='…'
  loop-once-icon='…'
  pip-icon='…'
  subtitle-icon='…'
  quality-icon='…'
  more-icon='…'
  theater-icon='…'
  screenshot-icon='…'
  airplay-icon='…'
  miniplayer-icon='…'>
</shadow-plyr>

🧑‍💻 JavaScript API

const player = document.querySelector("shadow-plyr") as ShadowPlyr;

// Playback
player.play();
player.pause();
player.seek(120);          // seek to 120 s

// Volume
player.mute();
player.unmute();

// Loop
player.setLoop(true);

// Properties (proxied from underlying <video>)
player.currentTime  // number
player.duration     // number
player.volume       // number (0–1)
player.muted        // boolean
player.paused       // boolean
player.ended        // boolean
player.src          // string
player.playbackRate // number

📡 Events

player.addEventListener("video-ready",         e => console.log(e.detail.duration));
player.addEventListener("video-playing",        e => console.log(e.detail.currentTime));
player.addEventListener("video-paused",         e => {});
player.addEventListener("video-ended",          e => {});
player.addEventListener("video-seeking",        e => {});
player.addEventListener("video-seeked",         e => {});
player.addEventListener("video-volume-change",  e => console.log(e.detail.volume, e.detail.muted));
player.addEventListener("video-error",          e => console.log(e.detail.code));
player.addEventListener("video-loop-change",    e => console.log(e.detail.loop));
player.addEventListener("video-fullscreen-enter", () => {});
player.addEventListener("video-fullscreen-exit",  () => {});
player.addEventListener("theater-mode-change",  e => console.log(e.detail.enabled));
player.addEventListener("mini-player-change",   e => console.log(e.detail.active));

⌨️ Keyboard Shortcuts

| Key | Action | |---|---| | Space / K | Play / Pause | | / | Seek backward / forward (seek-step seconds) | | / | Volume up / down | | M | Mute toggle | | F | Fullscreen toggle | | L | Loop toggle | | P | Picture-in-Picture | | T | Theater mode | | Home / End | Jump to start / end | | 09 | Seek to 0 % – 90 % | | ? | Show keyboard help overlay |


⚠️ Deprecated Attributes

The following attributes still work but will be removed in a future major version. Migrate to child elements instead.

| Deprecated attribute | Migration | |---|---| | desktop-poster | Use <picture><img src="…"></picture> as a child element | | mobile-poster | Use <picture><source media="(max-width:768px)" srcset="…"> | | desktop-video | Use <source src="…" media="(min-width:769px)"> as a child element | | mobile-video | Use <source src="…" media="(max-width:768px)"> as a child element |

A deprecation warning is printed to the console if these attributes are detected.


🌍 Browser Support

| Browser | Supported | |---|---| | Chrome / Edge | ✅ | | Firefox | ✅ | | Safari (macOS & iOS) | ✅ | | Android Chrome | ✅ |

AirPlay requires Safari on Apple devices. Picture-in-Picture requires browser API support.


🏗 Built With

  • Web Components (Custom Elements v1, Shadow DOM v1)
  • TypeScript
  • Constructable Stylesheets (adoptedStyleSheets)
  • DOMPurify (XSS-safe icon / loader HTML)
  • hls.js (optional – peer dependency for HLS streams)

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Commit using Conventional Commits: git commit -m "feat: add xyz"
  4. Open a Pull Request against develop

See CONTRIBUTING.md for full guidelines.


📄 License

MIT © Element Mint