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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@magic-spells/responsive-video

v0.1.0

Published

Responsive video and poster image swapping web component.

Readme

@magic-spells/responsive-video

npm version npm bundle size license

A lightweight, zero-dependency web component that intelligently swaps video sources and poster images based on viewport width. Built for performance-critical hero sections and modern web applications where mobile users should never download desktop assets (and vice versa).

Live Demo

Why This Exists

Traditional responsive video implementations using <source> tags with media queries load multiple sources, wasting bandwidth and degrading performance on mobile devices. This component solves that by:

  • Loading only what's needed: Mobile users download only mobile videos and posters, desktop users get desktop assets
  • Automatic switching: Responds to viewport changes and updates the active source seamlessly
  • Performance-first: Uses requestAnimationFrame for resize throttling and passive event listeners
  • Framework-agnostic: Pure web component that works with any framework or no framework at all
  • Tiny footprint: Single class, no dependencies, ~2KB minified

Installation

npm install @magic-spells/responsive-video

Or use directly from a CDN:

<script type="module" src="https://unpkg.com/@magic-spells/responsive-video"></script>

Quick Start

<responsive-video
  mobile-video="https://cdn.example.com/video-portrait.mp4"
  desktop-video="https://cdn.example.com/video-landscape.mp4"
  mobile-poster="https://cdn.example.com/poster-portrait.jpg"
  desktop-poster="https://cdn.example.com/poster-landscape.jpg"
  breakpoint="900"
>
  <video autoplay muted playsinline loop></video>
</responsive-video>

The component automatically:

  1. Detects the viewport width
  2. Loads the appropriate video source and poster image
  3. Applies the src and poster to the child <video> element
  4. Re-evaluates when the window resizes
  5. Sets data-active-mode to "mobile" or "desktop" for styling hooks

API

Attributes

| Attribute | Type | Default | Description | |-----------|------|---------|-------------| | mobile-video | String | — | Video source URL for viewports narrower than the breakpoint | | desktop-video | String | — | Video source URL for viewports equal to or wider than the breakpoint | | mobile-poster | String | — | Poster image URL for viewports narrower than the breakpoint (optional) | | desktop-poster | String | — | Poster image URL for viewports equal to or wider than the breakpoint (optional) | | breakpoint | Number | 768 | Viewport width (in pixels) where the switch between mobile and desktop occurs |

Behavior

  • If viewport width ≥ breakpoint → loads desktop-video and desktop-poster
  • If viewport width < breakpoint → loads mobile-video and mobile-poster
  • If mobile-video is missing → falls back to desktop-video on all screen sizes
  • Poster attributes are optional—if omitted, no poster is set
  • If no matching video exists → component does nothing
  • The component looks for the first <video> element in its light DOM

Data Attributes

The component sets data-active-mode on itself to indicate which source is currently active:

<responsive-video data-active-mode="mobile">...</responsive-video>

You can use this for conditional styling:

responsive-video[data-active-mode="mobile"] {
  /* Mobile-specific styles */
}

responsive-video[data-active-mode="desktop"] {
  aspect-ratio: 16/9;
}

How It Works

The ResponsiveVideo class extends HTMLElement and implements the Custom Elements API with these lifecycle hooks:

Lifecycle

  1. connectedCallback(): Queries for the child <video> element, attaches resize listeners, and performs initial source evaluation
  2. disconnectedCallback(): Cleans up event listeners and cancels pending animation frames

The component does not use observedAttributes or attributeChangedCallback—attributes are read dynamically on connect and during resize events.

Resize Handling

Window resize events are throttled using requestAnimationFrame to prevent excessive recalculation. The component only updates the video source when it detects an actual change (e.g., crossing the breakpoint threshold or when the video URL differs from the currently loaded source).

Source Swapping

When the source needs to change:

  1. The component updates the <video> element's src attribute
  2. Updates the poster attribute (if a poster URL is provided for the active mode)
  3. Calls video.load() to initiate loading
  4. Attempts to auto-play if video.autoplay is true (catches and ignores errors for muted autoplay requirements)
  5. Updates data-active-mode to reflect the active source ("mobile" or "desktop")

Private Implementation Details

The component uses private class fields (denoted by #) to encapsulate state:

  • #videoEl: Reference to the child video element
  • #currentSrc: Currently loaded video URL to prevent redundant updates
  • #resizeRaf: requestAnimationFrame ID for resize throttling
  • #boundResize: Cached resize handler reference

Use Cases

E-commerce Hero Sections

<responsive-video
  mobile-video="/assets/videos/hero-mobile.mp4"
  desktop-video="/assets/videos/hero-desktop.mp4"
  mobile-poster="/assets/images/hero-mobile-poster.jpg"
  desktop-poster="/assets/images/hero-desktop-poster.jpg"
  breakpoint="768"
>
  <video autoplay muted playsinline loop></video>
</responsive-video>

Progressive Enhancement

<responsive-video
  mobile-video="/videos/hero-mobile.mp4"
  desktop-video="/videos/hero-desktop.mp4"
  mobile-poster="/images/hero-mobile-poster.jpg"
  desktop-poster="/images/hero-desktop-poster.jpg"
>
  <video autoplay muted playsinline loop>
    <!-- Fallback for browsers without custom element support -->
    <source src="/videos/hero-desktop.mp4" type="video/mp4">
  </video>
</responsive-video>

Development

Install dependencies:

npm install

Start the dev server with live reload:

npm run dev

This launches a local server on port 3006 with hot module replacement. The server serves both dist/ and demo/ directories. The demo uses CDN-hosted videos to avoid committing large files to the repository.

Project Structure

responsive-video/
├── src/
│   └── responsive-video.js    # Source code (ES class)
├── demo/
│   └── index.html             # Demo page (uses CDN video URLs)
├── dist/                      # Build outputs (generated)
│   ├── responsive-video.esm.js    # ES Module
│   ├── responsive-video.cjs.js    # CommonJS
│   ├── responsive-video.js        # UMD
│   └── responsive-video.min.js    # Minified UMD
├── rollup.config.mjs          # Rollup bundler config
└── package.json

Build Commands

# Production build (creates all distribution formats)
npm run build

# Lint source code
npm run lint

# Format code with Prettier
npm run format

# Start dev server
npm run dev

Build Outputs

The build process generates four distribution formats:

  1. ESM (responsive-video.esm.js) — For modern bundlers and <script type="module">
  2. CommonJS (responsive-video.cjs.js) — For Node.js and older bundlers
  3. UMD (responsive-video.js) — Universal module for direct browser usage
  4. Minified UMD (responsive-video.min.js) — Production-ready minified bundle

All builds include sourcemaps except the minified version.

Browser Support

Supports all modern browsers that implement Custom Elements v1:

  • Chrome/Edge 67+
  • Firefox 63+
  • Safari 10.1+
  • iOS Safari 10.3+

For older browsers, use a Custom Elements polyfill.

Performance Considerations

  • Component registers with the Custom Elements registry only once
  • Resize events are throttled via requestAnimationFrame
  • Source updates are skipped when no actual change is detected
  • Event listeners use { passive: true } for better scroll performance
  • Video element is cached to avoid repeated DOM queries
  • Cleanup happens automatically on disconnect

License

MIT © Cory Schulz

Contributing

Issues and pull requests are welcome at github.com/magic-spells/responsive-video.