Skip to content

Honor prefers-reduced-motion automatically - #231

Merged
nk-o merged 2 commits into
masterfrom
feat/prefers-reduced-motion
Aug 14, 2026
Merged

nk-o merged 2 commits into
masterfrom
feat/prefers-reduced-motion

Conversation

@nk-o

@nk-o nk-o commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

A reader whose system asks for reduced motion still got the full parallax and an autoplaying background video. Both now stop on their own, with nothing to configure.

On the parallax side the implementation adds no new rendering state: (prefers-reduced-motion: reduce) folds into the disableParallax check, so a reduced-motion visitor lands on exactly the layout that option has always produced — a path already used for the mobile fallback and already covered by tests. The video side is decided separately, because what should replace the video depends on the provider, and disableVideo should keep meaning only what the author asked for.

Background Reduced motion shows
Image The image, positioned as usual, not moving
YouTube / Vimeo The provider thumbnail; the iframe is never requested
Self-hosted, with a fallback image That image; the video is never requested
Self-hosted, no fallback image The video's own first frame, inserted paused

Skipping the provider iframe rather than inserting-and-pausing it is deliberate: the thumbnail is already painted as the container background, so there is nothing to gain from several hundred kilobytes of player for someone who will not see it move.

The self-hosted case

A self-hosted video has no thumbnail to borrow, so an element with no image of its own would have gone blank — trading motion sickness for an empty hero is not a win. It is inserted paused instead, which works because of one measured detail:

paused <video> first frame painted
preload="metadata", no poster yes, at currentTime=0
preload="metadata", placeholder poster no

The placeholder poster is a transparent 1×1 GIF, and it is what would have covered the frame. So in this one case the poster is left off and object-fit: cover is set directly — started never fires, so coverImage() never gets a chance to size the element itself.

Verified in Chrome 151

Playwright's reducedMotion emulation sets the real media feature, so this exercises the actual query rather than a stub.

reduce           img container=false moved=false   remote iframe=false  speed=1
                 self-hosted: inserted, poster=null, object-fit=cover,
                              paused=true, t=0.00, frame painted=YES
no-preference    img container=true  moved=true    remote iframe=true   speed=0.4
                 self-hosted: inserted, poster set, playing, t=0.90

The frame check samples the element through a canvas and measures luminance range, so "painted" means real pixels, not just a readyState. The control run is the point: nothing changes when the preference is not set.

Reduced motion is read when the instance is created. Toggling the system setting applies on the next page load; making it live means destroying and re-creating instances behind React's and jQuery's backs, which did not seem worth it for how rarely the setting changes mid-session.

Also in here

  • videoYoutubeHost and videoVimeoHost, forwarded to the options added in video-worker 3.1.0. They follow the existing video* prefix so the data attributes stay grouped (data-video-youtube-host). Left unset they pass undefined, which video-worker's extend skips, so its own default is never pinned here a second time.
  • video-worker bumped to ^3.1.0 — Vimeo backgrounds no longer play muted in Chrome, and YouTube moves off the nocookie host.
  • npm install alphabetized two @types/react* lines in package.json. Incidental, not mine.

npm run lint, format:check, typecheck and test:run (50 tests) pass. The new tests cover both directions for image and video backgrounds, the poster-only insertion, and that no player is requested or played under reduced motion.

Unrelated, noticed while testing: calling jarallaxVideo() by hand on top of the UMD bundle, which already calls it, patches the prototype twice and corrupts defaultInitImgResult. Pre-existing, not touched here.

Readers who ask their system for reduced motion now get the same
treatment the library already had behind `disableParallax: true` and
`disableVideo: true`: the image is covered and positioned as usual but
never moves with the scroll, and the background video is never inserted,
so the poster stays on screen and no provider player is downloaded.

Also pass the new video-worker host options through as
`videoYoutubeHost` and `videoVimeoHost`, and pick up video-worker 3.1.0.
@vercel

vercel Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
jarallax Ready Ready Preview Aug 14, 2026 10:18am

Request Review

A self-hosted video has no provider thumbnail to fall back on, so an
element with no image of its own went blank under reduced motion. It is
now inserted paused instead: browsers paint the first frame of a video
that is not playing, as long as the placeholder poster is left off, so
the block keeps something to look at.

Reduced motion is no longer folded into `disableVideo`, because what
replaces the video depends on the provider and that option should keep
meaning only what the author asked for.
@nk-o
nk-o merged commit 00efe1a into master Aug 14, 2026
3 checks passed
@nk-o
nk-o deleted the feat/prefers-reduced-motion branch August 18, 2026 08:22

This branch was successfully deployed

1 active deployment
Preview — d534614f Deployed Aug 14, 2026 by vercel[bot]
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