# Scroll Cinematic Experience

> Build an advanced, scroll-driven cinematic experience: a fullscreen video hero that transitions, as the user scrolls, into a second phase (a black panel that slides up revealing a scattered image gallery whose cards scale in and out of view), ending in an outro overlay with a CTA. Includes a custom cursor and, optionally, cursor-scrubbed dual video. Built on GSAP ScrollTrigger + Motion, RAF-driven (not scroll events). Use this skill whenever the user wants a premium "scrollytelling" site, a fashion/archive/portfolio experience, a multi-phase scroll animation, pinned/sticky scroll sequences, or something that "transforms as you scroll". This is the most cinematic, highest-effort option — reach for it when the brief screams award-site.

- Skill: `brcapitalusa/scroll-cinematic-experience` (Agent Skill)
- Install (CLI): `npx skillmds@latest add brcapitalusa/scroll-cinematic-experience`
- Raw SKILL.md: https://api.skillmd.com/api/skills/brcapitalusa/scroll-cinematic-experience/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: BRCapitalUSA (https://skillmd.com/u/brcapitalusa)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/brcapitalusa/scroll-cinematic-experience

---


# Scroll-Cinematic Experience

A two-phase scroll narrative. Phase 1: video hero with overlaid UI. Phase 2: a
black panel slides up over the video and holds a scattered image grid whose
cards scale in as they enter and out as they leave. An outro fades a white
overlay + "view" CTA at the end. Everything is driven by a RAF loop reading
`scrollY`, not by scroll events — that's what keeps it buttery.

## Before building
Read `/mnt/skills/public/frontend-design/SKILL.md`. This is complex — confirm
the user actually wants scroll-driven behavior (not a static hero) before
committing. Collect: hero `VIDEO_URL`(s), a set of `GALLERY_IMAGES` (8–12),
`BRAND` wordmark, a caption, product/collection labels + price, and the outro
CTA word (e.g. "view"). Palette: black + white, overlays use `mix-blend-mode:
exclusion` so text stays legible over both.

## Stack
React 19 + TypeScript + Vite, Tailwind (v4 via `@tailwindcss/vite` is fine),
`gsap` + `@gsap/react` (ScrollTrigger), `motion` (`motion/react`). Fonts: a
tight grotesk like "Inter Tight". Single-page, one big component tree.

## Structure & phases
Root `#scroll-spacer`: `position: relative`, `user-select: none`, tall height
set dynamically to `vh + maxScroll + 2*vh` (via GSAP after measuring the
gallery), `cursor: none` on desktop.

- **Hero layer** (`position: fixed, inset: 0, z-0`): the video container.
  Entrance-animate the overlaid UI with Motion (logo `0s`, nav `0.15s`, caption
  `0.3s`, product info `0.45s`; `opacity 0→1, y 12→0`, ease
  `[0.25,0.1,0.25,1]`). All overlays are `pointer-events-none` +
  `mix-blend-mode: exclusion`.
- **Black panel** (`position: fixed, inset: 0, bg-black, z-10`): starts
  `translateY(100vh)`. A GSAP ScrollTrigger (`scrub: true`) slides it to
  `translateY(0)` over the first `100vh` of scroll.
- **Gallery** inside the panel: a responsive grid (2 / 3 / 4 cols) built by a
  layout function that scatters images (one per row at a computed column, a
  second every 3rd row). Each `.bp-card` is `scale(0)` with a bottom
  transform-origin (left-half cards origin `right bottom`, right-half `left
  bottom`).

## The RAF card-scale loop (core mechanic)
Do NOT use scroll events. Each frame, read `scrollY` and set each card's scale
from its viewport position:
```js
const vh = window.innerHeight;
// enter: grows 0→1 as the card rises into view
const enter = Math.min(1, (vh - top) / (vh * 0.6));
// exit: shrinks 1→0 as it leaves the top
const exit  = Math.min(1, bottom / (vh * 0.4));
const scale = (bottom <= 0 || top >= vh) ? 0 : Math.min(enter, exit);
card.style.transform = `scale(${scale})`;
```
Phase 1 (`scrollY < vh`): compute `top/bottom` with a panel offset of
`vh - scrollY`. Phase 2 (`scrollY > vh`): pin the panel, translate the inner
wrapper up by `-(scrollY - vh)`. Outro (`scrollY > vh + maxScroll`): fade a
white overlay in, slide the product info up, scale the "view" pill from 0→1,
fade the footer in; progress `= (scrollY - vh - maxScroll) / (vh - 100)`.

## Custom cursor & optional video scrubbing
Desktop cursor: a `fixed pointer-events-none z-50` element positioned by
`mousemove` (direct `style.left/top`), `mix-blend-mode: exclusion`, containing a
small SVG glyph. Optional dual-video interaction: scrub two `<video>`s by cursor
X with a center dead zone (`Math.max(30, width*0.05)`), only setting
`currentTime` when `!video.seeking` to avoid jitter; on touch, auto-alternate
the two clips on `ended`.

## CSS
```css
.bp-card { will-change: transform; }
@media (prefers-reduced-motion: reduce) { .bp-card { will-change: auto; } }
```

## Responsive
Breakpoints 640 / 1024. Custom cursor desktop-only; touch gets default cursor +
autoplaying video. Grid columns step 2→3→4. Use RAF position tracking
throughout; never rely on native scroll snapping.

## Quality checklist
- [ ] Panel slides up over the video during the first viewport of scroll.
- [ ] Cards scale in on enter and out on exit, computed per-frame in RAF.
- [ ] Outro overlay + CTA appear at the end; footer fades in.
- [ ] Overlays use `mix-blend-mode: exclusion` and `pointer-events-none`.
- [ ] Works with reduced-motion and on touch (no custom cursor there).
- [ ] Spacer height set so all phases have room to play out.

