Scroll-Animated Site Builder
Take a topic and a subject from the user and ship a complete, verified, scroll-driven website — without handing the work back mid-way.
Before you start: is this a new site or a retrofit?
Adding motion to a page that already ranks is a different job with different
risks — the scroll budget must include the existing page height, and a pin is
dead scroll distance on a page someone is scanning to make a decision. Default to
building at a separate showcase URL and linking to it; convert in place only
when the page has no traffic to lose and its job is impression rather than
reference. Always take a byte-for-byte backup first and hand the user the restore
command. Read references/08-failure-modes.md §3 before agreeing to a retrofit.
The pipeline
Six phases. Run them in order. Only Phase 1 talks to the user.
1 DISCOVER → wizard, 2–3 rounds of questions [interactive]
2 SCORE → compile scroll-score.json blueprint [autonomous]
3 ASSETS → generate/collect images, frames, video [autonomous]
4 SCAFFOLD → project skeleton + motion runtime [autonomous]
5 BUILD → implement every beat in the Score [autonomous]
6 VERIFY → drive a real browser, fix, repeat [autonomous]
After the wizard closes in Phase 1, stop asking questions. Phases 2–6 run to
completion on your own judgment. Record assumptions in DECISIONS.md and report
them at the end instead of blocking on them. The user asked for an end-to-end
build; a half-built page plus a question is a failed run.
Phase 1 — DISCOVER (the wizard)
Ask with the AskUserQuestion tool, never as a wall of prose. Two rounds is
typical, three is the maximum. Full question bank, branch by branch:
→ Read references/01-discovery-wizard.md
Round 1 is always the same two questions — subject type and primary goal:
| Subject | What the scroll is really doing |
|---|---|
| Person (portfolio, résumé, personal brand) | Earning trust: work → proof → contact |
| Product (physical or digital) | Revealing the object: hero → features → buy |
| Company (agency, startup, studio) | Establishing scale: manifesto → work → team |
| Event / Launch (conference, drop, release) | Building anticipation: date → lineup → register |
| Cause / Story (nonprofit, editorial, report) | Carrying an argument: hook → evidence → act |
The branch you land on picks the section skeleton and the dominant motion pattern. Round 2 asks branch-specific questions (what work, what product, what proof). Round 3, only if still ambiguous, settles aesthetic direction, motion intensity, and asset strategy.
Stop the wizard as soon as you can name: the subject, the goal, 4–7 sections, an aesthetic direction, and where assets come from. Infer the rest.
Phase 2 — SCORE (the blueprint)
Compile everything into scroll-score.json at the project root. This is the
contract the build and the verifier both read — write it before writing any
component.
→ Read references/02-scroll-score.md for the full schema.
A Score is a list of beats. One beat = one scroll-driven moment: what pins, what scrubs, over what distance, and what moves.
{
"meta": { "subject": "…", "type": "product", "goal": "…", "stack": "vite-vanilla" },
"theme": { "bg": "#0a0a0a", "fg": "#fafafa", "accent": "#ff4d17", "font": { … } },
"motion":{ "intensity": "cinematic", "smoothScroll": false, "reducedMotion": "degrade" },
"beats": [
{ "id": "hero", "pattern": "media-scrub-sequence", "pin": true,
"scrub": 1, "start": "top top", "end": "+=200%", "frames": 48, … },
{ "id": "features", "pattern": "sticky-stack", … }
]
}
Budget the scroll. Total page height should land near
100vh × (1 + Σ beat scroll cost). A pinned scrub beat costs 1.5–2.5 viewport
heights; a reveal beat costs ~1. Under 3 total the site feels abrupt; over 12 it
feels like a chore. Aim for 5–9.
Phase 3 — ASSETS
Never ship grey boxes. Every beat named in the Score needs its real asset before Phase 5 — a build against placeholders always has to be redone.
→ Read references/05-asset-pipeline.md for the full pipeline, including
the AI-render → interpolated video → frame-sequence chain, all ffmpeg
invocations, and the weight budgets.
Three sources, in order of preference:
- User-supplied — always wins. Ask for them in the wizard, never after.
- Generated —
mcp__image-gen-mcp__generate_image(gpt-image-2,background: "transparent"for products, up to3840x2160), oredit_imageto turn a real product photo into a styled render. - Placeholder — only for a beat the user explicitly deferred, and it must be a considered gradient/type composition, never a grey box.
For an image-sequence hero (the Apple-style "product explodes as you scroll"), the chain is: first frame + last frame → interpolated video → extracted frames → WebP sequence → canvas scrub. Keep the sequence under 4 MB total, 36–60 frames, 1600px wide. That budget is the whole reason the effect stays usable; blowing it is the single most common failure of this pattern.
Phase 4 — SCAFFOLD
Default stack, unless the user named one: Vite + vanilla TS + GSAP. It has
no framework lifecycle to fight, and ScrollTrigger bugs in React are almost
always cleanup bugs. Use Next.js/React only when the user asks or the
project needs routing, and then use useGSAP() for cleanup — never bare
useEffect.
Smooth scroll (Lenis) is OPT-IN — it is no longer part of the default stack.
Add it only when the user asks for smooth or inertial scrolling by name. It
replaces native scrolling with its own animated position, and when it feels even
slightly wrong the whole page reads as broken — this is the single most common
reason a technically-correct build gets rejected. ScrollTrigger's scrub already
gives you the eased, cinematic feel people usually mean by "smooth".
See references/08-failure-modes.md §1.
→ Read references/07-stack-setup.md for scaffolds and exact commands.
GSAP is 100% free since v3.13 (Webflow acquisition), including SplitText,
ScrollSmoother and MorphSVG. npm install gsap gets everything — no license
key, no gsap-trial, no CDN plugin workarounds. Do not tell the user a plugin
is paid.
Content lives in src/content.ts, never inline in markup. The user must be able
to change copy without touching motion code.
Phase 5 — BUILD
Implement each beat from the Score using the canonical pattern implementations:
→ Read references/03-pattern-library.md — 12 patterns, copy-ready.
| Pattern | Use it for |
|---|---|
reveal-on-enter | Standard section entrances, card grids |
text-reveal | Headlines, pull quotes (SplitText, masked) |
pin-scrub | The core "wow" moment — pinned stage, scrubbed timeline |
parallax-layers | Depth: backgrounds, foreground props |
media-scrub-sequence | Apple-style product reveal (canvas + frames) |
media-scrub-video | Same effect from a video file (currentTime) |
horizontal-scroll | Portfolios, galleries, timelines |
mask-reveal | An image opening up to swallow the viewport |
sticky-stack | Feature lists, stacking cards |
scroll-progress | Progress bars, counters, section indicators |
theme-morph | Background/color shifting between sections |
snap-sections | Deck-like, one-section-per-scroll pages |
And the non-negotiable GSAP rules:
→ Read references/04-gsap-rules.md before writing ScrollTrigger code.
The five that break builds most often:
- One ScrollTrigger per timeline, on the timeline — never on a child tween, never nested inside a parent timeline.
- If — and only if — you added Lenis, it shares GSAP's loop.
lenis.on('scroll', ScrollTrigger.update)+gsap.ticker.add(t => lenis.raf(t * 1000))+gsap.ticker.lagSmoothing(0). Never add a separaterequestAnimationFrameloop for Lenis. ease: "none"on any tween drivingcontainerAnimationor a media scrub.scrubortoggleActions, never both on the same trigger.ScrollTrigger.refresh()after fonts and images load — not just on resize.- Before any pin, walk the ancestors for
transform/filter/contain. Any of them silently breaksposition: fixed, so the pin is created, reportspin: true, and does nothing. Remove the property or setpinType: 'transform'. See08-failure-modes.md§2.
Animate transform and opacity only. Anything that touches top, left,
width, height, or margin on scroll will drop frames.
Phase 6 — VERIFY
This phase is not optional, and it is what separates this skill from writing GSAP by hand. Scroll animations cannot be verified by reading code — pins collapse, triggers fire at the wrong depth, and frames go missing only in a real browser at a real viewport.
→ Read references/06-verification.md for the full loop and the check list.
→ Read references/08-failure-modes.md — failures that pass every automated
check. Read it before Phase 4, not after something breaks.
Start the dev server, then drive it with Playwright MCP:
mcp__playwright__browser_navigate → the dev server URL
mcp__playwright__browser_resize → 1440×900, then 390×844
mcp__playwright__browser_evaluate → scroll to depth, read state
mcp__playwright__browser_take_screenshot → at each depth
mcp__playwright__browser_console_messages → errors and GSAP warnings
mcp__playwright__browser_network_requests → 404s, asset weight
Sweep 0 %, 15 %, 30 %, 50 %, 70 %, 85 %, 100 % of page height at desktop and mobile. At each stop, screenshot and look at it. You are checking that the page looks right, not merely that it threw no errors.
Drive the page with REAL input — mouse.wheel() or key presses, never
window.scrollTo. Synthetic scrolling is swallowed by scroll-hijacking
libraries, which is precisely the failure you need to catch. If three wheel
notches do not move the page, it is broken no matter what the assertions say.
Fail the run and fix if any of these is true:
- A console error, or any
ScrollTriggerwarning - Any 404, or total asset weight over budget
- A blank/unstyled screenshot at any depth
- A pinned section that overlaps the next section, or collapses the layout
- Content that never becomes visible (a reveal that never fires)
- Horizontal overflow at 390px wide
markers: truesurviving anywhere in the source- No
prefers-reduced-motionfallback - Three real wheel notches do not visibly move the page
- A pin whose stage still tracks scroll (assert
|rect.top| < 10inside the range) - Scrubbed media reaching its end before ~90% of its pin
- More than ~40 wheel notches to reach the primary CTA
Loop fix → re-verify until clean, up to 5 rounds. If something still fails after 5, ship the rest, disable that one beat behind a flag, and say so plainly in the final report. Never report success on an unverified page.
Definition of done
-
scroll-score.jsonmatches what was actually built - Every beat verified at desktop and mobile
- Zero console errors, zero 404s
-
prefers-reduced-motiondegrades to instant states, no motion - Lighthouse-ish sanity: no layout shift from pins, images sized
-
README.md— how to run, where content lives, how to swap assets -
DECISIONS.md— assumptions made after the wizard closed - Final message: what was built, what was assumed, what to review
Do not
- ❌ Ask questions after the wizard closes — assume, log, and report instead
- ❌ Report done without running Phase 6 in a real browser
- ❌ Ship
markers: true,console.log, or aTODOin delivered code - ❌ Use
gsap-trialor claim SplitText/ScrollSmoother need a Club licence - ❌ Animate layout properties on scroll
- ❌ Pin on mobile without checking it — prefer fading in under 768px
- ❌ Exceed the asset budget for a hero sequence