Skip to content

Release 2.6.0: pause button, reduced motion, lazy loading, HLS and style.css - #49

Merged
pmochine merged 22 commits into
masterfrom
features-2026-10
Oct 7, 2026
Merged

pmochine merged 22 commits into
masterfrom
features-2026-10

Conversation

@pmochine

@pmochine pmochine commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Feature release 2.6.0. All new options are off by default. Without them, the component works as in 2.5.1.

Related issues: #44 (HLS), #14 (keep the larger video), #30 (video element), #3 (play and pause). Please close them after the release, not with this merge.

New options

Option What it does
pauseButton, pauseLabel, playLabel, slot pause-button A native button that pauses and plays the video, for WCAG 2.2.2. It is first in the tab order inside the section. Its label changes between the two labels, without aria-pressed. A pause with the button stays across source switches and across pauseWhenHidden.
respectReducedMotion With "reduce motion" in the system settings, only the poster shows, and the video does not load. The button or player.play() starts it. A live change of the setting pauses a playing video.
pauseWhenHidden Pauses the video while it is off screen or the page is in the background. Browsers do not do this for a video that starts with play(). A video that the user paused stays paused.
lazy The video loads only near the viewport: 200px before the section comes into view.
keepLargerSource A smaller window keeps a larger video that already loads (#14).
hls, hlsConfig You pass the Hls class of hls.js (#44). Where hls.js is supported, it plays .m3u8 sources. Elsewhere, the browser plays them, as before. The package never imports hls.js.
player.video The <video> element (#30).
vue-responsive-video-background-player/style.css The CSS as a file, with a type declaration for TypeScript 6 and 7. With server-side rendering, the server HTML has its styles before the JavaScript runs. The JavaScript still injects the CSS.

play() on a video that waits (lazy, reduced motion) or that failed loads it first. The play button also loads a failed video again.

Fixed

  • pause() and stop() before the video is ready keep it paused. Before, autoplay started it at readiness.
  • play() before the video is ready plays it at readiness, also with autoplay set to false. Before, the video stayed paused.

Development tools

  • jsdom 30. The development tools need Node.js 22.22.2, 24.15.0 or 26 or newer. .nvmrc names 24.21.0. CI tests Node.js 22, 24 and 26.
  • TypeScript stays on 5.9 for the development tools. vue-tsc 3.3.12 stops with ERR_PACKAGE_PATH_NOT_EXPORTED for typescript/lib/tsc, because TypeScript 7 no longer ships the JavaScript API of the compiler. The types of the package are tested with TypeScript 5.9, 6.0 and 7.0.
  • ESLint 10 and eslint-config-avidofood 5. The lint results do not change.
  • hls.js 1.7.3 is a dev dependency for the type tests only.

Checks

  • 147 Vitest tests (62 before), lint, type tests. The SSR and hydration tests also pass with the production build of Vue.
  • Tarball: 9 files, about 20 kB. dist/ imports only vue, without ?., ?? or process. ESM 19.3 kB (gzip 5.7 kB, before 3.4 kB).
  • Import test with the packed tarball, Vue 3.5.43 and Vue 3.2.0: ESM, CommonJS, SSR, UMD global, style.css, and a client mount in jsdom.
  • Types against the tarball with TypeScript 5.9, 6.0 and 7.0 (bundler, node16 ESM and CJS, node10), with Vue 3.5 and 3.2.0. vue-tsc 3.3.12 checks an SFC template with all new props.
  • @arethetypeswrong/cli: no problems for the main entry. publint: no errors and no warnings.
  • Headless Chrome 154 on the demo page and on test pages, 35 checks:
    • focus order, focus rings, keyboard
    • pause and resume from the same point
    • source switches, keepLargerSource
    • lazy with real hls.js over MSE, including the loop
    • pause off screen and in a background tab
    • emulated reduced motion
    • server HTML with and without style.css, hydration
    • retry after a missing MP4 and a missing HLS manifest
  • Not tested on real Safari, iOS or Firefox devices.

Review

An independent review by Codex (gpt-6-astra) checked the work before this pull request:

  • A full review found seven problems (three major). All are fixed with tests.
  • Two short reviews of the fixes found four minor problems and one regression in a fix. These are fixed, and a last short review checked the final fix of play().

Demo

The hero shows the pause button, reduced motion, pauseWhenHidden and keepLargerSource. A second section plays an HLS stream with hls.js from jsDelivr. The stream has 1 MB, and ffmpeg encoded it from an existing demo video. The section loads it only near the viewport.

Release after the merge

  1. Create the trusted publisher on npmjs.com right before the tag (release.yml, environment npm-publish).
  2. Tag 2.6.0 on master and push the tag. The Release workflow publishes.

🤖 Generated with Claude Code

https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4

pmochine and others added 22 commits October 7, 2026 16:43
A component of an earlier test kept its listeners on window and
document, so it reacted to the events of the next test. The helpers
add fakes for IntersectionObserver, matchMedia and the page visibility,
which jsdom does not have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Before, the element was only reachable through the internal
player.$refs.video.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
You pass the Hls class of hls.js with the new hls prop, and hlsConfig
for its options. If the browser supports hls.js, hls.js plays .m3u8
sources. Otherwise the browser plays them itself, as before. The
package does not import hls.js, so the bundle stays without it.

hls.js removes all <source> elements when it detaches. The component
renders its <source> again only after hls.js stopped. A fatal hls.js
error keeps the poster and emits error with a CustomEvent, whose
detail holds the error data of hls.js.

hls.js is a dev dependency for the type tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
All four options are off by default, so the component behaves as
before.

- pauseButton renders a native button before the content, first in
  the tab order (WCAG 2.2.2 Pause, Stop, Hide). Its label changes
  between pauseLabel and playLabel, and the pause-button slot replaces
  the icon. A pause with the button stays when the window switches to
  another source.
- respectReducedMotion shows only the poster when the user prefers
  reduced motion, and the video does not load. The play button or
  play() loads and plays it. A change of the setting pauses a playing
  video.
- pauseWhenHidden pauses the video while it is off screen or the page
  is in the background, and plays it again when it is visible. A video
  that the user paused stays paused.
- lazy loads the video when the section comes within 200px of the
  viewport.

play() on a video that waits loads it first. Its promise resolves
when the video plays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
With keepLargerSource, a smaller window keeps a video that already
loads, so the browser does not download the smaller one too. A larger
window still switches to the larger video. Off by default.

The new tests also cover a source with an empty src: the component
loads no video for that window width and shows only the poster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The build also writes the CSS of the component to dist/style.css, and
the package exports it as vue-responsive-video-background-player/style.css.
With server-side rendering, the page can link it, so the server HTML
has its styles before the JavaScript runs. With a strict Content
Security Policy that blocks injected <style> elements, the styles
still apply. The JavaScript still injects the CSS, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The styles of the button are inside :where(), so they have no
specificity. A plain .videobg-pause-button rule in your CSS now wins,
whatever the order of the style sheets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The README gets sections for the pause button and accessibility, for
less loading and less work, for HLS streams with hls.js, for the CSS
file and for the slots. The Nuxt section shows the CSS file in
nuxt.config.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The browser test showed a square button on the demo page: Bootstrap 4
sets button { border-radius: 0 }, and that rule beat the :where()
rules, which have no specificity. Tailwind also sets the background of
[type='button'] to transparent. The styles of the button now use
button.videobg-pause-button in a style block without scope. That beats
the resets, and a selector with two classes still overrides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The hero gets the pause button, respectReducedMotion, pauseWhenHidden
and keepLargerSource, and posters for the desktop and tablet videos.
Without a poster, reduced motion showed an empty section. A second
section below plays an HLS stream (1 MB, encoded with ffmpeg from
accessories-hero-desktop.mp4) with hls.js and lazy loading, and lists
its events. The demo loads hls.js 1.7.3 from jsDelivr, so the demo
bundle stays without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
TypeScript 6 and 7 check side-effect imports by default, so
import 'vue-responsive-video-background-player/style.css' failed with
TS2882 in a project without its own *.css declaration. The build now
writes dist/style.css.d.ts, and the export points to it with the types
condition. Bundlers and Node.js still get dist/style.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
An independent review by Codex (gpt-6-astra, high) found seven problems.

- pause() and stop() before the video was ready did not stick: autoplay
  started the video when it was ready, also after play() on a lazy
  video. play(), pause() and stop() now tell the background what you
  want, for the current video. After a switch to another source, the
  autoplay of that source decides again, as before. The pause button
  keeps its choice across switches. The background itself uses the new
  internal startVideo() and pauseVideo().
- After a fatal hls.js error or a video that failed to load, the play
  button showed "pause" and did nothing. play() and the button now load
  the video again.
- A play() that waited for the video stayed pending when the window
  switched to a source with only a poster, or to another video. It
  resolves now.
- A fatal hls.js error inside loadSource() got lost, because the
  instance was not active yet. The instance is active before
  loadSource() now.
- play() of the player did not count as the choice of the user, so
  turning on reduced motion paused a video that play() started.
- Back from a poster-only source within one second, hls.js started
  twice. The pending load() now handles the new source.
- Turning lazy off did not load the video.

The tests for all seven fail on the code before this commit. The fake
of hls.js now removes the <source> elements like hls.js with
ManagedMediaSource, and a reduced motion test flushes between changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
A short review by Codex (gpt-6-astra, medium) of the previous fixes
found four minor problems.

- The old play() of a failed stream ended the wait of a newer play()
  too early, when the new load interrupted it. Only the current play
  request ends the wait now.
- reloadAfterFailure() left a pending load() active, so hls.js started
  twice. It cancels the pending load() now, and hides the video.
- A choice through the player stayed when the window switched to
  another video while lazy loading still held the source back. The
  choice now follows the selected source, not the loaded one.
- The test for play() before readiness passed also without the fix,
  because the media mock resolved play() at once. It now models a
  pending play() that the pause at readiness interrupts.

The new tests fail on the code before this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The second short review by Codex (gpt-6-astra, medium) found a major
problem in the guard from the previous commit: when the browser blocked
playback, the catch branch hid the video, which made its own play
request obsolete. Then the guard never ended the wait, and a play()
on a lazy or reduced-motion video stayed pending forever.

This was the last review round, so the guard goes back to the line
that the first short review checked. The small problem that the guard
fixed is open again: the old play() of a failed stream can end the
wait of a newer play() too early. Its test is now it.fails. New tests
check that a waiting play() resolves when the browser blocks playback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
jsdom 30 needs Node.js ^22.22.2, ^24.15.0 or >=26. .nvmrc names
24.21.0, and CI tests Node.js 22, 24 and 26. The release workflow
already uses the newest Node.js 24. The published files do not change.

TypeScript stays on 5.9 for the development tools. vue-tsc 3.3.12, the
newest version, stops with ERR_PACKAGE_PATH_NOT_EXPORTED for
typescript/lib/tsc, because TypeScript 7 no longer ships the
JavaScript API of the compiler. The type tests of this repository pass
with TypeScript 7, and the types of the package are tested with
TypeScript 5.9, 6.0 and 7.0.

ESLint stays on 9 until eslint-config-avidofood 5 supports ESLint 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
The old play() of a failed stream ended the wait of a newer play() too
early: the new load interrupted the old play() of the browser, and its
promise chain resolved all waiting plays. Now only the current play
request ends the wait.

The first try of this guard in 661e39d also left the wait open when the
browser blocked playback, because hide() in the catch branch made the
request obsolete. Now the catch branch ends the wait before hide().

The test of the open problem is it() again, and the two tests for a
blocked play() stay green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
eslint-config-avidofood 5 needs ESLint 10 and uses eslint-plugin-import-x.
The rule names stay import/..., and eslint.config.mjs does not change.
The lint result is the same as before: no errors and no warnings in 27
files, with the same 342 active rules. ESLint 10 needs Node.js ^20.19,
^22.13 or >=24, which the development tools already need for jsdom 30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WCUweANBn9U6qXMZNf9w4
@pmochine
pmochine merged commit d9e19fd into master Oct 7, 2026
3 checks passed
@pmochine
pmochine deleted the features-2026-10 branch October 8, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant