Skip to content

feat(home): add a calm and pleasant hero loop - #1074

Open
wiesson wants to merge 2 commits into
TanStack:mainfrom
wiesson:feat/hero-video-loop
Open

feat(home): add a calm and pleasant hero loop#1074
wiesson wants to merge 2 commits into
TanStack:mainfrom
wiesson:feat/hero-video-loop

Conversation

@wiesson

@wiesson wiesson commented Jul 29, 2026

Copy link
Copy Markdown

feat(home): add a calm and pleasant hero loop

What

The homepage hero palm now breathes. Same shot, same framing, but as a 12 second
video loop with a very gentle push-in — roughly 3% scale over the full cycle, so
it reads as "alive" rather than "animated".

Because why not.

How it loops

The source is a one-directional zoom, so loop on its own would visibly snap
back to frame 0. The shipped clip is a ping-pong instead — forward 0 → 144,
then reverse 143 → 1 — which makes the wrap seamless in both directions.

Measured as SSIM between the last and first frame:

last → first frame
raw source 0.914
ping-pong 0.985
adjacent frames mid-clip (baseline) 0.990

Poster and fallback

poster is frame 0 of the loop itself, not the existing
hero-palm-gradient-1600.webp. The two crops differ slightly, and using the old
still would cause a visible jump the moment playback starts.

If no <source> can be decoded, the <video> just keeps showing the poster —
which is exactly the current homepage. There is no worse-than-today state.

Encoding

The sky is a single continuous gradient, which bands badly under 8-bit
compression. Both files are therefore 10-bit:

File Codec Size Serves
hero-palm-gradient-loop.webm VP9 profile 2, 10-bit, crf 38 959 KB Chrome, Firefox, Edge
hero-palm-gradient-loop.mp4 HEVC Main10 (hvc1), crf 28 897 KB Safari (HW decode on Apple silicon)
hero-palm-gradient-loop.webp frame 0 24 KB poster / fallback

8-bit H.264 needed roughly 2.7 MB to reach comparable gradient smoothness, so
10-bit is both the better-looking and the smaller option here. Audio and the
cover-art stream are stripped.

The codec strings are written out explicitly in type:

video/webm; codecs="vp09.02.30.10"
video/mp4;  codecs="hvc1.2.4.L93.B0"

Without them Safari reports generic video/webm as playable, selects the WebM,
and then fails on the 10-bit VP9. With them, source selection resolves correctly
on the first try. Verified in Chromium via canPlayType and actual playback.

Reduced motion

prefers-reduced-motion: reduce pauses the loop on its first frame, so those
users get the still hero. This mirrors how PixelSpinner already handles the
same preference. autoplay stays declarative so the loop does not wait on
hydration; the effect only walks it back.

disableRemotePlayback keeps Safari from offering an AirPlay overlay on what is
a decorative background.

Assets kept

Nothing is deleted. hero-palm-gradient-960.webp is still used by
src/routes/ds.stats.tsx and -1600.webp by
scripts/generate-brand-assets.mjs for the OG image.

Known limitation

The source clip is 1168×768, below the 1600/2400 ladder the site ships for the
still. object-cover crops about 25% of the height in the ~2:1 hero card and
scales the remainder by ~1.33×, which absorbs most of it, but on a high-density
display it is softer than the current photo. Happy to re-encode from a
higher-resolution source if that matters for merge.

Summary by CodeRabbit

  • New Features

    • Updated the home page hero area with looping video playback and a poster image.
    • Added support for multiple video formats for broader browser compatibility.
  • Accessibility

    • Respects reduced-motion preferences by pausing the video and displaying its opening frame.

Swaps the still palm photo on the homepage hero for a 12s video loop of
the same shot, with a very gentle push-in (~3% over the full cycle).

The clip is built as a ping-pong (forward, then reverse) so the wrap is
seamless rather than snapping back to frame 0. The poster is frame 0 of
that loop, so the still and the first video frame are the same picture
and nothing pops when playback starts. If no source can be decoded the
poster simply stays up, which is exactly today's behaviour.

Encoding notes: the sky is one long gradient and bands badly in 8-bit,
so both files are 10-bit. VP9 profile 2 (959 KB) covers Chrome, Firefox
and Edge; HEVC Main10 (897 KB) covers Safari with hardware decode on
Apple silicon. An 8-bit H.264 file needed ~2.7 MB for comparable
smoothness. Codec strings are spelled out in `type` so Safari skips the
10-bit VP9 instead of selecting it and failing to decode.

`prefers-reduced-motion: reduce` holds the loop on its first frame,
matching how PixelSpinner handles the same preference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The home page hero changes from a responsive image to a looping video with WebM and MP4 sources, a poster frame, and reduced-motion handling. A new WebP image asset is added for the hero media.

Changes

Home page hero video

Layer / File(s) Summary
Hero video rendering and motion handling
src/routes/index.tsx, public/images/hero-palm-gradient-loop.webp
The hero uses a looping muted video with WebM and MP4 sources, a poster frame, and pauses at the first frame when reduced motion is enabled.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: tannerlinsley

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a looping home hero asset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routes/index.tsx`:
- Around line 67-77: Update the hero video logic around heroVideoRef to use the
existing usePrefersReducedMotion hook instead of a one-time matchMedia effect.
Keep autoplay disabled while the preference is unknown and whenever reduced
motion is enabled, and update the video playback behavior when the preference
changes so reduced-motion users do not start downloading or seeing motion
prematurely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74d72ce4-d1bb-4d17-a70f-ac0f957d31e4

📥 Commits

Reviewing files that changed from the base of the PR and between 4cbedd5 and a793b4c.

⛔ Files ignored due to path filters (2)
  • public/images/hero-palm-gradient-loop.mp4 is excluded by !**/*.mp4
  • public/images/hero-palm-gradient-loop.webm is excluded by !**/*.webm
📒 Files selected for processing (2)
  • public/images/hero-palm-gradient-loop.webp
  • src/routes/index.tsx

Comment thread src/routes/index.tsx
Comment on lines +67 to +77
const heroVideoRef = React.useRef<HTMLVideoElement>(null)

// Autoplay is declarative so the loop starts without waiting for hydration;
// this only walks it back for users who asked for less motion.
React.useEffect(() => {
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
const video = heroVideoRef.current
if (!video) return
video.pause()
video.currentTime = 0
}, [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate autoplay on the reduced-motion preference.

The effect runs only once, after the initial autoPlay/preload="auto" markup has already been processed, so reduced-motion users can briefly see motion and still download the video. Preference changes after mount are also ignored. Use the existing usePrefersReducedMotion hook, keep autoplay disabled while its value is unknown, and react to changes.

Suggested direction
+import { usePrefersReducedMotion } from '~/utils/usePrefersReducedMotion'
...
+  const prefersReducedMotion = usePrefersReducedMotion()
   const heroVideoRef = React.useRef<HTMLVideoElement>(null)

   React.useEffect(() => {
-    if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
+    if (prefersReducedMotion === null) return
     const video = heroVideoRef.current
     if (!video) return
-    video.pause()
-    video.currentTime = 0
-  }, [])
+    if (prefersReducedMotion) {
+      video.pause()
+      video.currentTime = 0
+    } else {
+      void video.play().catch(() => {})
+    }
+  }, [prefersReducedMotion])
...
-                  autoPlay
+                  autoPlay={prefersReducedMotion === false}
...
-                  preload="auto"
+                  preload={prefersReducedMotion === false ? 'auto' : 'none'}

Also applies to: 104-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/index.tsx` around lines 67 - 77, Update the hero video logic
around heroVideoRef to use the existing usePrefersReducedMotion hook instead of
a one-time matchMedia effect. Keep autoplay disabled while the preference is
unknown and whenever reduced motion is enabled, and update the video playback
behavior when the preference changes so reduced-motion users do not start
downloading or seeing motion prematurely.

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