Home page hero + feature/step sections render as empty gray gradient — IntersectionObserver animation never fires, content stays at opacity 0 #107

Closed
opened 2026-09-25 15:54:57 +00:00 by Mark · 1 comment
Owner

Bug: Home page hero <motion.h1> content never paints — framer-motion animate={{...}} direct-animate fails on first mount in React 19.2 + framer-motion 12.40

Reproduction

  1. cd ~/Projects/Kimi_Agent_Brother_10_Digitizing_App && npm run dev
  2. Open http://localhost:3000/ in a fresh browser tab.
  3. Wait ≥ 10 seconds (long enough for the 6.6 MB /hero-stitch-animation.mp4 to fully load and decode).
  4. Observe the viewport at scrollY=0.

Expected

The hero section is min-h-[100dvh] with a video background + dark overlay. Centered content rendered via framer-motion motion.p / motion.h1 / motion.div:

  • Eyebrow (orange #E6A948): "EMBROIDERY DIGITIZING FOR BROTHER ENTREPRENEUR"
  • Headline (cream #F8F2E9): "Turn Any Image Into Perfect Stitches"
  • Sub-headline: "Upload your artwork. Choose your threads. Download a PES file ready for your Brother PR-10."
  • Two CTAs: "Open Workspace" and "Watch Demo"
  • Background video playing.

Actual

At scrollY=0, the hero is empty gray gradient. Header (navbar) and the "Service unavailable" alert banner render normally. Below the banner, the entire viewport (~993px tall) is empty gray gradient. Eyebrow, headline, sub-headline, gradient underline, CTAs, and background video all invisible.

Verified with mcp__browseros__screenshot at t+2s, t+3s, t+6s, t+10s — content never appears. Cross-verified by navigating to /workspace (renders correctly) then back to / — hero still invisible. Cross-verified with a fresh tab + cache_bust query string — same result.

mcp__browseros__read (text extraction, scrolls internally) returns the full hero text correctly — DOM contains all the content. So this is a paint/animation failure, not a render failure.

Why this is NOT a duplicate of #104

#104 ("Three Steps section renders only 2 of 3 cards on first paint — Framer Motion + IntersectionObserver race") was closed with commit df02208 (per the closing comment "Fixed in df02208"). That commit switched Three Steps from shared-section IntersectionObserver + inView to per-card whileInView. Verified on main: src/pages/Home.tsx now has only whileInView calls, no setInView / shared IntersectionObserver remaining. Three Steps now renders correctly.

The hero does NOT use whileInView or IntersectionObserver. It uses framer-motion's direct animate={{ opacity: 1, y: 0 }} pattern with delays (200–900ms). This should ALWAYS run on mount, not on scroll-into-view. The fact that it doesn't render indicates a different failure mode — likely React 19 + framer-motion 12 direct-animate not running on initial mount, OR the hero <section> z-index/layout container is hiding painted content.

Root cause hypotheses

  1. React 19.2 + framer-motion 12.40 direct-animate regression. package.json shows "react": "^19.2.0" and "framer-motion": "^12.40.0". framer-motion 12 added LazyMotion + new mount semantics; React 19 changed concurrent rendering behavior. There's a known interaction where motion.X initial={{opacity:0}} animate={{opacity:1}} doesn't trigger the entry animation when the parent component renders inside a route transition.
  2. Hero video element blocks paint. The <video autoPlay loop muted playsInline> at src/pages/Home.tsx:43-53 is positioned absolute inset-0 w-full h-full object-cover with a min-h-[100dvh] parent. The 6.6 MB MP4 may be blocking the main thread during initial decode, delaying React's commit phase past the framer-motion mount tick.
  3. CSS conflict. A global CSS rule (video{aspect-ratio:16/9} from Tailwind preflight, found in dist/assets/*.css) may be conflicting with the hero video's w-full h-full object-cover classes, causing the video to render at aspect-ratio size instead of full-bleed — which in turn would expose the dark overlay as a small rectangle and the rest of the section as transparent (showing through to body's neutral background = gray).

Suggested fix scope

  1. Audit the direct-animate pattern on the hero. Switch from initial={{opacity:0}} animate={{opacity:1}} to initial={{opacity:1}} + whileInView={{opacity:1}} if the entry animation isn't load-bearing; OR keep the entry animation but add viewport={{once:true}} and explicitly trigger on mount via useAnimation().start().
  2. Investigate the video element. Add a <source poster="/hero-still.jpg"> so first paint shows a poster frame even if the video is decoding. Verify with a smaller video (compressed to ~500 KB).
  3. Add a regression test. Render Home.tsx in jsdom with a stubbed IntersectionObserver + HTMLVideoElement.play = vi.fn(), assert that the hero <h1> has computed opacity: 1 after 1 second.

Labels

  • bug
  • usability
  • triage-approved

Severity

High. This is the FIRST page a new user sees. With the hero invisible, the page presents as a half-finished stub: header chrome + alert banner + empty gray gradient. A first-time visitor cannot tell what the product does, doesn't see the value proposition, and has no path to engage. Critical path user-story failure on the marketing surface.

## Bug: Home page hero `<motion.h1>` content never paints — framer-motion `animate={{...}}` direct-animate fails on first mount in React 19.2 + framer-motion 12.40 ### Reproduction 1. `cd ~/Projects/Kimi_Agent_Brother_10_Digitizing_App && npm run dev` 2. Open `http://localhost:3000/` in a fresh browser tab. 3. Wait ≥ 10 seconds (long enough for the 6.6 MB `/hero-stitch-animation.mp4` to fully load and decode). 4. Observe the viewport at scrollY=0. ### Expected The hero section is `min-h-[100dvh]` with a video background + dark overlay. Centered content rendered via framer-motion `motion.p / motion.h1 / motion.div`: - Eyebrow (orange `#E6A948`): **"EMBROIDERY DIGITIZING FOR BROTHER ENTREPRENEUR"** - Headline (cream `#F8F2E9`): **"Turn Any Image Into Perfect Stitches"** - Sub-headline: **"Upload your artwork. Choose your threads. Download a PES file ready for your Brother PR-10."** - Two CTAs: **"Open Workspace"** and **"Watch Demo"** - Background video playing. ### Actual **At scrollY=0, the hero is empty gray gradient.** Header (navbar) and the "Service unavailable" alert banner render normally. Below the banner, the entire viewport (~993px tall) is empty gray gradient. Eyebrow, headline, sub-headline, gradient underline, CTAs, and background video all invisible. Verified with `mcp__browseros__screenshot` at t+2s, t+3s, t+6s, t+10s — content never appears. Cross-verified by navigating to `/workspace` (renders correctly) then back to `/` — hero still invisible. Cross-verified with a fresh tab + cache_bust query string — same result. `mcp__browseros__read` (text extraction, scrolls internally) returns the full hero text correctly — DOM contains all the content. So this is a **paint/animation failure, not a render failure**. ### Why this is NOT a duplicate of #104 #104 ("Three Steps section renders only 2 of 3 cards on first paint — Framer Motion + IntersectionObserver race") was closed with commit `df02208` (per the closing comment "Fixed in df02208"). That commit switched Three Steps from shared-section `IntersectionObserver + inView` to per-card `whileInView`. Verified on `main`: `src/pages/Home.tsx` now has only `whileInView` calls, no `setInView` / shared IntersectionObserver remaining. Three Steps now renders correctly. **The hero does NOT use `whileInView` or IntersectionObserver.** It uses framer-motion's direct `animate={{ opacity: 1, y: 0 }}` pattern with delays (200–900ms). This should ALWAYS run on mount, not on scroll-into-view. The fact that it doesn't render indicates a different failure mode — likely React 19 + framer-motion 12 direct-animate not running on initial mount, OR the hero `<section>` z-index/layout container is hiding painted content. ### Root cause hypotheses 1. **React 19.2 + framer-motion 12.40 direct-animate regression.** `package.json` shows `"react": "^19.2.0"` and `"framer-motion": "^12.40.0"`. framer-motion 12 added LazyMotion + new mount semantics; React 19 changed concurrent rendering behavior. There's a known interaction where `motion.X initial={{opacity:0}} animate={{opacity:1}}` doesn't trigger the entry animation when the parent component renders inside a route transition. 2. **Hero video element blocks paint.** The `<video autoPlay loop muted playsInline>` at `src/pages/Home.tsx:43-53` is positioned `absolute inset-0 w-full h-full object-cover` with a `min-h-[100dvh]` parent. The 6.6 MB MP4 may be blocking the main thread during initial decode, delaying React's commit phase past the framer-motion mount tick. 3. **CSS conflict.** A global CSS rule (`video{aspect-ratio:16/9}` from Tailwind preflight, found in `dist/assets/*.css`) may be conflicting with the hero video's `w-full h-full object-cover` classes, causing the video to render at aspect-ratio size instead of full-bleed — which in turn would expose the dark overlay as a small rectangle and the rest of the section as transparent (showing through to body's neutral background = gray). ### Suggested fix scope 1. **Audit the direct-animate pattern on the hero.** Switch from `initial={{opacity:0}} animate={{opacity:1}}` to `initial={{opacity:1}}` + `whileInView={{opacity:1}}` if the entry animation isn't load-bearing; OR keep the entry animation but add `viewport={{once:true}}` and explicitly trigger on mount via `useAnimation().start()`. 2. **Investigate the video element.** Add a `<source poster="/hero-still.jpg">` so first paint shows a poster frame even if the video is decoding. Verify with a smaller video (compressed to ~500 KB). 3. **Add a regression test.** Render `Home.tsx` in jsdom with a stubbed `IntersectionObserver` + `HTMLVideoElement.play = vi.fn()`, assert that the hero `<h1>` has computed `opacity: 1` after 1 second. ### Labels - bug - usability - triage-approved ### Severity **High.** This is the FIRST page a new user sees. With the hero invisible, the page presents as a half-finished stub: header chrome + alert banner + empty gray gradient. A first-time visitor cannot tell what the product does, doesn't see the value proposition, and has no path to engage. Critical path user-story failure on the marketing surface.
Mark 2026-09-25 15:54:57 +00:00
  • closed this issue
  • added the
    bug
    label
Mark 2026-09-25 17:05:15 +00:00
Author
Owner

Re-opening and rescoping after the auto-close daemon saw the IntersectionObserver overlap with #104.

This issue is distinct from #104:

  • #104 was: Three Steps shared-section IntersectionObserver + setInView race. Fixed in df02208 by switching to per-card whileInView.
  • This issue (#107): Hero <motion.h1> etc. uses framer-motion direct animate={{opacity: 1}} with delays (200–900ms), NOT whileInView. The hero content never paints even after 10s wait. Verified that all other sections on Home now use whileInView and are correct — only the hero is broken.

Re-titled and re-bodied to reflect the actual novel finding. Repro evidence preserved in screenshots taken 2026-09-25 against main @ 1a31160.

— automated cleanup, 2026-09-25

Re-opening and rescoping after the auto-close daemon saw the IntersectionObserver overlap with #104. This issue is **distinct from #104**: - #104 was: Three Steps shared-section `IntersectionObserver + setInView` race. Fixed in `df02208` by switching to per-card `whileInView`. - **This issue (#107)**: Hero `<motion.h1>` etc. uses framer-motion **direct `animate={{opacity: 1}}`** with delays (200–900ms), NOT `whileInView`. The hero content never paints even after 10s wait. Verified that all other sections on Home now use `whileInView` and are correct — only the hero is broken. Re-titled and re-bodied to reflect the actual novel finding. Repro evidence preserved in screenshots taken 2026-09-25 against `main` @ `1a31160`. — automated cleanup, 2026-09-25
Mark 2026-10-01 00:16:58 +00:00
Sign in to join this conversation.
No description provided.