scroll-motion
Add motion that runs on the compositor thread, points toward the action, and
degrades gracefully — not a heavy JS library for a fade.
The failure this fixes
Asked to "add scroll animation", a model reaches for the heaviest tool it knows —
Framer Motion for a decorative fade, Three.js for a hero — when a few lines of
native CSS would run for free on the compositor thread. It then animates
layout-triggering properties (width, height, top, box-shadow), forcing
every frame onto the main thread, which competes with interaction handling and
tanks INP and CLS on the mobile devices that are 60%+ of traffic. The second half
of the scar is narrative: it scroll-jacks and gates content behind a scroll
"tunnel" with no exit to pricing/CTA — the most-hated anti-pattern (NN/g) and an
accessibility failure.
When to use / when NOT to use
Use for any scroll/motion decision: reveals, parallax, progress bars,
pinned/scrubbed sequences, scrollytelling, smooth-scroll, and choosing between
CSS / GSAP / Motion.
Not for: the words (landing-page-copywriting), section order
(landing-page-structure), palette/type (visual-design-system), general
performance/SEO (web-vitals-and-seo — this skill owns motion's INP impact
specifically), or full WCAG work (landing-page-accessibility — this skill owns
the motion accessibility floor).
Workflow
Classify the animation by purpose, then pick the tier (climb from the top;
drop down only when the tier above structurally can't do it). CSS covers ~80%
of what ScrollTrigger used to be needed for.
| Need |
Reach for |
INP impact |
| Decorative reveals, fade/slide-in, progress bar, simple parallax, staggered grids |
Native CSS animation-timeline: scroll() / view() |
None (compositor thread) |
| Pinned sections, scrubbed timelines, horizontal-in-vertical, SVG path |
GSAP + ScrollTrigger (via @gsap/react useGSAP()) |
Low if optimized |
React component state, gestures, layout animation, AnimatePresence exits |
Motion (formerly Framer Motion) |
Higher; use LazyMotion |
| Immersive 3D scroll scenes (brand/premium only) |
react-three-fiber, sparingly |
High; prefer pre-rendered sequences |
Hybrid stacking all tiers on one page is the normal professional pattern.
Keep it compositor-safe. Animate only transform, opacity, filter.
Compositor eligibility is all-or-nothing per @keyframes block — one
layout-triggering property drops the whole animation to the main thread.
Handle the CSS gotchas. animation-fill-mode: both is mandatory (or
reveals flicker back to their start state); scroll() tracks container
progress, view() tracks one element through the viewport; wrap in
@supports (animation-timeline: scroll()) with a static, fully-visible
fallback (Firefox lacks it). Details: references/motion-stack.md.
Gate motion for accessibility. prefers-reduced-motion is the floor, not
sufficient — reduce (swap displacement for an opacity cross-fade), don't
delete; reveal content must be structurally present regardless of animation;
auto-play >5s needs a pause control (WCAG 2.2.2). Full rules:
references/motion-stack.md.
Never gate content. Motion points toward the action; keep visible exit
points and CTAs; never scroll-jack. Cap scrollytelling at ~5 scenes / 2-3 min,
and apply the litmus test: if removing the scroll-link still reads fine as a
static page, it was decoration, not scrollytelling — write the narrative in
prose first.
The rules
- Purpose picks the tier, not what's already imported. A simple reveal in
Framer Motion is the canonical waste.
- Only
transform / opacity / filter animate cheaply; everything else
triggers layout/paint every frame.
animation-fill-mode: both on every scroll reveal.
@supports fallback because Firefox lacks animation-timeline; unsupported
browsers render the final visible state, never a hidden one.
- Lenis smooth-scroll breaks
position: sticky, IntersectionObserver, and CSS
scroll-driven animations unless run in native mode (ReactLenis root). Only
add it to coordinate GSAP + Motion under one rAF loop.
- GSAP is free for commercial use (Webflow/GreenSock, since April 2025),
including ScrollTrigger and SplitText — it is not MIT-licensed; it uses the
GreenSock standard license.
useGSAP() auto-cleans tweens on unmount.
- In Next.js/RSC, JS animation libraries force a contagious
"use client"
boundary — push it to the leaf; CSS scroll-driven animation forces none (another
reason to default to it). Boundary mechanics live in nextjs-landing-page.
Output
Motion implemented at the lowest-cost tier that fits the purpose, animating only
compositor-safe properties, with @supports fallbacks and reduced-motion gating,
no scroll-jacking, and any browser-support claim version-gated against caniuse.
References
references/motion-stack.md - the full tiered decision, CSS gotchas, the
accessibility floor, per-library reduced-motion hooks, GSAP pin pitfalls, and
the fact ledger (stable vs drift-prone vs unverifiable).
1---2name: scroll-motion3description: Adds scroll-triggered animation and motion to a landing page with the right tier by purpose - native CSS scroll-driven animation, GSAP/ScrollTrigger, or Motion - protecting INP and honoring prefers-reduced-motion. Use when asked to add scroll animations, reveals, parallax, a pinned/scrubbed sequence, smooth scrolling, or pick an animation library. Not for copy, layout, palette, or SEO.4---56# scroll-motion78Add motion that runs on the compositor thread, points toward the action, and9degrades gracefully — not a heavy JS library for a fade.1011## The failure this fixes1213Asked to "add scroll animation", a model reaches for the heaviest tool it knows —14Framer Motion for a decorative fade, Three.js for a hero — when a few lines of15native CSS would run for free on the compositor thread. It then animates16layout-triggering properties (`width`, `height`, `top`, `box-shadow`), forcing17every frame onto the main thread, which competes with interaction handling and18tanks INP and CLS on the mobile devices that are 60%+ of traffic. The second half19of the scar is narrative: it scroll-jacks and gates content behind a scroll20"tunnel" with no exit to pricing/CTA — the most-hated anti-pattern (NN/g) and an21accessibility failure.2223## When to use / when NOT to use2425Use for any scroll/motion decision: reveals, parallax, progress bars,26pinned/scrubbed sequences, scrollytelling, smooth-scroll, and choosing between27CSS / GSAP / Motion.2829Not for: the words (`landing-page-copywriting`), section order30(`landing-page-structure`), palette/type (`visual-design-system`), general31performance/SEO (`web-vitals-and-seo` — this skill owns motion's INP impact32specifically), or full WCAG work (`landing-page-accessibility` — this skill owns33the motion accessibility floor).3435## Workflow36371. **Classify the animation by purpose, then pick the tier** (climb from the top;38 drop down only when the tier above structurally can't do it). CSS covers ~80%39 of what ScrollTrigger used to be needed for.4041 | Need | Reach for | INP impact |42 |---|---|---|43 | Decorative reveals, fade/slide-in, progress bar, simple parallax, staggered grids | **Native CSS** `animation-timeline: scroll()` / `view()` | None (compositor thread) |44 | Pinned sections, scrubbed timelines, horizontal-in-vertical, SVG path | **GSAP + ScrollTrigger** (via `@gsap/react` `useGSAP()`) | Low if optimized |45 | React component state, gestures, layout animation, `AnimatePresence` exits | **Motion** (formerly Framer Motion) | Higher; use `LazyMotion` |46 | Immersive 3D scroll scenes (brand/premium only) | **react-three-fiber**, sparingly | High; prefer pre-rendered sequences |4748 Hybrid stacking all tiers on one page is the normal professional pattern.49502. **Keep it compositor-safe.** Animate only `transform`, `opacity`, `filter`.51 Compositor eligibility is all-or-nothing per `@keyframes` block — one52 layout-triggering property drops the whole animation to the main thread.533. **Handle the CSS gotchas.** `animation-fill-mode: both` is mandatory (or54 reveals flicker back to their start state); `scroll()` tracks container55 progress, `view()` tracks one element through the viewport; wrap in56 `@supports (animation-timeline: scroll())` with a static, fully-visible57 fallback (Firefox lacks it). Details: `references/motion-stack.md`.584. **Gate motion for accessibility.** `prefers-reduced-motion` is the floor, not59 sufficient — reduce (swap displacement for an opacity cross-fade), don't60 delete; reveal content must be structurally present regardless of animation;61 auto-play >5s needs a pause control (WCAG 2.2.2). Full rules:62 `references/motion-stack.md`.635. **Never gate content.** Motion points *toward* the action; keep visible exit64 points and CTAs; never scroll-jack. Cap scrollytelling at ~5 scenes / 2-3 min,65 and apply the litmus test: if removing the scroll-link still reads fine as a66 static page, it was decoration, not scrollytelling — write the narrative in67 prose first.6869## The rules7071- **Purpose picks the tier, not what's already imported.** A simple reveal in72 Framer Motion is the canonical waste.73- **Only `transform` / `opacity` / `filter`** animate cheaply; everything else74 triggers layout/paint every frame.75- **`animation-fill-mode: both`** on every scroll reveal.76- **`@supports` fallback** because Firefox lacks `animation-timeline`; unsupported77 browsers render the final visible state, never a hidden one.78- **Lenis smooth-scroll breaks `position: sticky`, `IntersectionObserver`, and CSS79 scroll-driven animations** unless run in native mode (`ReactLenis root`). Only80 add it to coordinate GSAP + Motion under one rAF loop.81- **GSAP is free** for commercial use (Webflow/GreenSock, since April 2025),82 including ScrollTrigger and SplitText — it is *not* MIT-licensed; it uses the83 GreenSock standard license. `useGSAP()` auto-cleans tweens on unmount.84- **In Next.js/RSC**, JS animation libraries force a contagious `"use client"`85 boundary — push it to the leaf; CSS scroll-driven animation forces none (another86 reason to default to it). Boundary mechanics live in `nextjs-landing-page`.8788## Output8990Motion implemented at the lowest-cost tier that fits the purpose, animating only91compositor-safe properties, with `@supports` fallbacks and reduced-motion gating,92no scroll-jacking, and any browser-support claim version-gated against caniuse.9394## References9596- `references/motion-stack.md` - the full tiered decision, CSS gotchas, the97 accessibility floor, per-library reduced-motion hooks, GSAP pin pitfalls, and98 the fact ledger (stable vs drift-prone vs unverifiable).