From dbc05e5c017574ffd43650a799fb5ec695e63076 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 3 Apr 2026 19:45:13 +0530 Subject: [PATCH 1/2] feat: refine gsap-framer-scroll-animation skill and references --- docs/README.skills.md | 1 + skills/gsap-framer-scroll-animation/SKILL.md | 139 ++++ .../references/framer.md | 674 +++++++++++++++++ .../references/gsap.md | 685 ++++++++++++++++++ 4 files changed, 1499 insertions(+) create mode 100644 skills/gsap-framer-scroll-animation/SKILL.md create mode 100644 skills/gsap-framer-scroll-animation/references/framer.md create mode 100644 skills/gsap-framer-scroll-animation/references/gsap.md diff --git a/docs/README.skills.md b/docs/README.skills.md index e3fd29872..7eeb21707 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -152,6 +152,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [github-copilot-starter](../skills/github-copilot-starter/SKILL.md) | Set up complete GitHub Copilot configuration for a new project based on technology stack | None | | [github-issues](../skills/github-issues/SKILL.md) | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, set issue fields (dates, priority, custom fields), set issue types, manage issue workflows, link issues, add dependencies, or track blocked-by/blocking relationships. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", "set the priority", "set the start date", "link issues", "add dependency", "blocked by", "blocking", or any GitHub issue management task. | `references/dependencies.md`
`references/images.md`
`references/issue-fields.md`
`references/issue-types.md`
`references/projects.md`
`references/search.md`
`references/sub-issues.md`
`references/templates.md` | | [go-mcp-server-generator](../skills/go-mcp-server-generator/SKILL.md) | Generate a complete Go MCP server project with proper structure, dependencies, and implementation using the official github.com/modelcontextprotocol/go-sdk. | None | +| [gsap-framer-scroll-animation](../skills/gsap-framer-scroll-animation/SKILL.md) | 'Use this skill whenever the user wants to build scroll animations, scroll effects, parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, or any motion tied to scroll position — in vanilla JS, React, or Next.js. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, useSpring, whileInView, variants). Use this skill even if the user just says "animate on scroll", "fade in as I scroll", "make it scroll like Apple", "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish.' | `references/framer.md`
`references/gsap.md` | | [gtm-0-to-1-launch](../skills/gtm-0-to-1-launch/SKILL.md) | Launch new products from idea to first customers. Use when launching products, finding early adopters, building launch week playbooks, diagnosing why adoption stalls, or learning that press coverage does not equal growth. Includes the three-layer diagnosis, the 2-week experiment cycle, and the launch that got 50K impressions and 12 signups. | None | | [gtm-ai-gtm](../skills/gtm-ai-gtm/SKILL.md) | Go-to-market strategy for AI products. Use when positioning AI products, handling "who is responsible when it breaks" objections, pricing variable-cost AI, choosing between copilot/agent/teammate framing, or selling autonomous tools into enterprises. | None | | [gtm-board-and-investor-communication](../skills/gtm-board-and-investor-communication/SKILL.md) | Board meeting preparation, investor updates, and executive communication. Use when preparing board decks, writing investor updates, handling bad news with the board, structuring QBRs, or building board-level metric discipline. Includes the "Three Things" narrative model, the 4-tier metric hierarchy, and the pre-brief pattern that prevents board surprises. | None | diff --git a/skills/gsap-framer-scroll-animation/SKILL.md b/skills/gsap-framer-scroll-animation/SKILL.md new file mode 100644 index 000000000..92e6f1b5a --- /dev/null +++ b/skills/gsap-framer-scroll-animation/SKILL.md @@ -0,0 +1,139 @@ +--- +name: gsap-framer-scroll-animation +description: >- + 'Use this skill whenever the user wants to build scroll animations, scroll effects, + parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, + or any motion tied to scroll position — in vanilla JS, React, or Next.js. + Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, + ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, + useSpring, whileInView, variants). Use this skill even if the user just says + "animate on scroll", "fade in as I scroll", "make it scroll like Apple", + "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". + Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. + Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish.' +--- + +# GSAP & Framer Motion — Scroll Animations Skill + +Production-grade scroll animations with GitHub Copilot prompts, ready-to-use code recipes, and deep API references. + +> **Design Companion:** This skill provides the *technical implementation* for scroll-driven motion. +> For the *creative philosophy*, design principles, and premium aesthetics that should guide **how** +> and **when** to animate, always cross-reference the [premium-frontend-ui](../premium-frontend-ui/SKILL.md) skill. +> Together they form a complete approach: premium-frontend-ui decides the **what** and **why**; +> this skill delivers the **how**. + +## Quick Library Selector + +| Need | Use | +|---|---| +| Vanilla JS, Webflow, Vue | **GSAP** | +| Pinning, horizontal scroll, complex timelines | **GSAP** | +| React / Next.js, declarative style | **Framer Motion** | +| whileInView entrance animations | **Framer Motion** | +| Both in same Next.js app | See notes in references | + +Read the relevant reference file for full recipes and Copilot prompts: + +- **GSAP** → `references/gsap.md` — ScrollTrigger API, all recipes, React integration +- **Framer Motion** → `references/framer.md` — useScroll, useTransform, all recipes + +## Setup (Always Do First) + +### GSAP +```bash +npm install gsap +``` +```js +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +gsap.registerPlugin(ScrollTrigger); // MUST call before any ScrollTrigger usage +``` + +### Framer Motion (Motion v12, 2025) +```bash +npm install motion # new package name since mid-2025 +# or: npm install framer-motion — still works, same API +``` +```js +import { motion, useScroll, useTransform, useSpring } from 'motion/react'; +// legacy: import { motion } from 'framer-motion' — also valid +``` + +## The 5 Most Common Scroll Patterns + +Quick reference — full recipes with Copilot prompts are in the reference files. + +### 1. Fade-in on enter (GSAP) +```js +gsap.from('.card', { + opacity: 0, y: 50, stagger: 0.15, duration: 0.8, + scrollTrigger: { trigger: '.card', start: 'top 85%' } +}); +``` + +### 2. Fade-in on enter (Framer Motion) +```jsx + +``` + +### 3. Scrub / scroll-linked (GSAP) +```js +gsap.to('.hero-img', { + scale: 1.3, opacity: 0, ease: 'none', + scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true } +}); +``` + +### 4. Scroll-linked (Framer Motion) +```jsx +const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] }); +const y = useTransform(scrollYProgress, [0, 1], [0, -100]); +return ; +``` + +### 5. Pinned timeline (GSAP) +```js +const tl = gsap.timeline({ + scrollTrigger: { trigger: '.section', pin: true, scrub: 1, start: 'top top', end: '+=200%' } +}); +tl.from('.title', { opacity: 0, y: 60 }).from('.img', { scale: 0.85 }); +``` + +## Critical Rules (Apply Always) + +- **GSAP**: always call `gsap.registerPlugin(ScrollTrigger)` before using it +- **GSAP scrub**: always use `ease: 'none'` — easing feels wrong when scrub is active +- **GSAP React**: use `useGSAP` from `@gsap/react`, never plain `useEffect` — it auto-cleans ScrollTriggers +- **GSAP debug**: add `markers: true` during development; remove before production +- **Framer**: `useTransform` output must go into `style` prop of a `motion.*` element, not a plain div +- **Framer Next.js**: always add `'use client'` at top of any file using motion hooks +- **Both**: animate only `transform` and `opacity` — avoid `width`, `height`, `box-shadow` +- **Accessibility**: always check `prefers-reduced-motion` — see each reference file for patterns +- **Premium polish**: follow the [premium-frontend-ui](../premium-frontend-ui/SKILL.md) principles for motion timing, easing curves, and restraint — animation should enhance, never overwhelm + +## Copilot Prompting Tips + +- Give Copilot the full selector, base image, and scroll range upfront — vague prompts produce vague code +- For GSAP, always specify: selector, start/end strings, whether you want scrub or toggleActions +- For Framer, always specify: which hook (useScroll vs whileInView), offset values, what to transform +- Paste the exact error message when asking `/fix` — Copilot fixes are dramatically better with real errors +- Use `@workspace` scope in Copilot Chat so it reads your existing component structure + +## Reference Files + +| File | Contents | +|---|---| +| `references/gsap.md` | Full ScrollTrigger API reference, 10 recipes, React (useGSAP), Lenis, matchMedia, accessibility | +| `references/framer.md` | Full useScroll / useTransform API, 8 recipes, variants, Motion v12 notes, Next.js tips | + +## Related Skills + +| Skill | Relationship | +|---|---| +| [premium-frontend-ui](../premium-frontend-ui/SKILL.md) | Creative philosophy, design principles, and aesthetic guidelines — defines *when* and *why* to animate | diff --git a/skills/gsap-framer-scroll-animation/references/framer.md b/skills/gsap-framer-scroll-animation/references/framer.md new file mode 100644 index 000000000..a62b600ab --- /dev/null +++ b/skills/gsap-framer-scroll-animation/references/framer.md @@ -0,0 +1,674 @@ +# Framer Motion (Motion v12) — Full Reference + +> Framer Motion was renamed to **Motion** in mid-2025. The npm package is now `motion`, +> the import path is `motion/react`. All APIs are identical. `framer-motion` still works. + +## Table of Contents +1. [Package & Import Paths](#package--import-paths) +2. [Two Types of Scroll Animation](#two-types-of-scroll-animation) +3. [useScroll — Options Reference](#usescroll--options-reference) +4. [useTransform — Full Reference](#usetransform--full-reference) +5. [useSpring for Smoothing](#usespring-for-smoothing) +6. [Recipes with Copilot Prompts](#recipes-with-copilot-prompts) + - Scroll progress bar + - Reusable ScrollReveal wrapper + - Parallax layers + - Horizontal scroll section + - Image reveal with clipPath + - Scroll-linked navbar (hide/show) + - Staggered card grid + - 3D tilt on scroll +7. [Variants Pattern for Stagger](#variants-pattern-for-stagger) +8. [Motion Value Events](#motion-value-events) +9. [Next.js & App Router Notes](#nextjs--app-router-notes) +10. [Accessibility](#accessibility) +11. [Common Copilot Pitfalls](#common-copilot-pitfalls) + +--- + +## Package & Import Paths + +```bash +npm install motion # recommended (renamed 2025) +npm install framer-motion # still works — same API +``` + +```js +// Recommended (Motion v12+) +import { motion, useScroll, useTransform, useSpring, useMotionValueEvent } from 'motion/react'; + +// Legacy — still valid +import { motion, useScroll, useTransform } from 'framer-motion'; +``` + +**Motion v12 new features (2025):** +- Hardware-accelerated scroll via browser ScrollTimeline API +- `useScroll` and `scroll()` now GPU-accelerated by default +- New color types: `oklch`, `oklab`, `color-mix` animatable directly +- Full React 19 + concurrent rendering support + +--- + +## Two Types of Scroll Animation + +### Scroll-triggered (fires once when element enters viewport) + +```jsx + + Content + +``` + +`viewport.margin` — negative value triggers animation before element fully enters view. +`viewport.once` — `true` means animate once, never reverse. + +### Scroll-linked (continuous, tied to scroll position) + +```jsx +const { scrollYProgress } = useScroll(); +const opacity = useTransform(scrollYProgress, [0, 1], [0, 1]); +return Content; +``` + +The value updates on every scroll frame — must use `style` prop, not `animate`. + +--- + +## useScroll — Options Reference + +```js +const { + scrollX, // Absolute horizontal scroll (pixels) + scrollY, // Absolute vertical scroll (pixels) + scrollXProgress, // Horizontal progress 0→1 between offsets + scrollYProgress, // Vertical progress 0→1 between offsets +} = useScroll({ + // Track a scrollable element instead of the viewport + container: containerRef, + + // Track an element's position within the container + target: targetRef, + + // Define when tracking starts and ends + // Format: ["target position container position", "target position container position"] + offset: ['start end', 'end start'], + // Common offset pairs: + // ['start end', 'end start'] = track while element is anywhere in view + // ['start end', 'end end'] = track from element entering to bottom of page + // ['start start', 'end start'] = track while element exits top + // ['center center', 'end start']= track from center-center to exit + + // Update when content size changes (small perf cost, false by default) + trackContentSize: false, +}); +``` + +**Offset string values:** +- `start` = `0` = top/left edge +- `center` = `0.5` = middle +- `end` = `1` = bottom/right edge +- Numbers 0–1 also work: `[0, 1]` = `['start', 'end']` + +--- + +## useTransform — Full Reference + +```js +// Map a motion value from one range to another +const y = useTransform(scrollYProgress, [0, 1], [0, -200]); + +// Multi-stop interpolation +const opacity = useTransform( + scrollYProgress, + [0, 0.2, 0.8, 1], + [0, 1, 1, 0] +); + +// Non-numeric values (colors, strings) +const color = useTransform( + scrollYProgress, + [0, 0.5, 1], + ['#6366f1', '#ec4899', '#f97316'] +); + +// CSS string values +const clipPath = useTransform( + scrollYProgress, + [0, 1], + ['inset(0% 100% 0% 0%)', 'inset(0% 0% 0% 0%)'] +); + +// Disable clamping (allow values outside output range) +const y = useTransform(scrollYProgress, [0, 1], [0, -200], { clamp: false }); + +// Transform from multiple inputs +const combined = useTransform( + [scrollX, scrollY], + ([x, y]) => Math.sqrt(x * x + y * y) +); +``` + +**Rule:** `useTransform` output is a `MotionValue`. It must go into the `style` prop of a `motion.*` element. Plain `
` will NOT work — must be ``. + +--- + +## useSpring for Smoothing + +Wrap any MotionValue in `useSpring` to add spring physics — great for progress bars that feel alive. + +```js +const { scrollYProgress } = useScroll(); + +const smooth = useSpring(scrollYProgress, { + stiffness: 100, // Higher = faster/snappier response + damping: 30, // Higher = less bounce + restDelta: 0.001 // Precision threshold for stopping +}); + +return ; +``` + +For a subtle lag (not physics), use `useTransform` with `clamp: false` and an eased range instead. + +--- + +## Recipes with Copilot Prompts + +### 1. Scroll Progress Bar + +**Copilot Chat Prompt:** +``` +Framer Motion: fixed scroll progress bar at top of page. +useScroll for page scroll progress, useSpring to smooth scaleX. +stiffness 100, damping 30. Grows left to right. +``` + +```tsx +'use client'; +import { useScroll, useSpring, motion } from 'motion/react'; + +export function ScrollProgressBar() { + const { scrollYProgress } = useScroll(); + const scaleX = useSpring(scrollYProgress, { + stiffness: 100, damping: 30, restDelta: 0.001, + }); + + return ( + + ); +} +``` + +--- + +### 2. Reusable ScrollReveal Wrapper + +**Copilot Chat Prompt:** +``` +Framer Motion: reusable ScrollReveal component that wraps children with +fade-in-up entrance animation using whileInView. Props: delay (default 0), +duration (default 0.6), once (default true). viewport margin -80px. +TypeScript. 'use client'. +``` + +```tsx +'use client'; +import { motion } from 'motion/react'; + +interface ScrollRevealProps { + children: React.ReactNode; + delay?: number; + duration?: number; + once?: boolean; + className?: string; +} + +export function ScrollReveal({ + children, delay = 0, duration = 0.6, once = true, className +}: ScrollRevealProps) { + return ( + + {children} + + ); +} + +// Usage: +//

Section Title

+``` + +--- + +### 3. Parallax Layers + +**Copilot Chat Prompt:** +``` +Framer Motion parallax section: background moves y from 0% to 30% (slow), +foreground text moves y from 50 to -50px (fast). +Both use target ref with offset ['start end', 'end start']. +Fade out at top and bottom using opacity useTransform [0, 0.3, 0.7, 1] → [0,1,1,0]. +``` + +```tsx +'use client'; +import { useRef } from 'react'; +import { motion, useScroll, useTransform } from 'motion/react'; + +export function ParallaxSection() { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ['start end', 'end start'], + }); + + const backgroundY = useTransform(scrollYProgress, [0, 1], ['0%', '30%']); + const textY = useTransform(scrollYProgress, [0, 1], [50, -50]); + const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]); + + return ( +
+ + +

Parallax Title

+

Scrolls at a different speed

+
+
+ ); +} +``` + +--- + +### 4. Horizontal Scroll Section + +**Copilot Chat Prompt:** +``` +Framer Motion horizontal scroll: 4 cards scroll horizontally as user scrolls vertically. +Outer container ref height 300vh controls speed (sticky pattern). +useScroll tracks outer container, useTransform maps scrollYProgress to x '0%' → '-75%'. +``` + +```tsx +'use client'; +import { useRef } from 'react'; +import { motion, useScroll, useTransform } from 'motion/react'; + +const cards = [ + { id: 1, title: 'Card One', color: 'bg-indigo-500' }, + { id: 2, title: 'Card Two', color: 'bg-pink-500' }, + { id: 3, title: 'Card Three', color: 'bg-amber-500' }, + { id: 4, title: 'Card Four', color: 'bg-teal-500' }, +]; + +export function HorizontalScroll() { + const containerRef = useRef(null); + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ['start start', 'end end'], + }); + + const x = useTransform(scrollYProgress, [0, 1], ['0%', '-75%']); + + return ( +
+
+ + {cards.map(card => ( +
+

{card.title}

+
+ ))} +
+
+
+ ); +} +``` + +--- + +### 5. Image Reveal with clipPath + +**Copilot Chat Prompt:** +``` +Framer Motion: image reveals left to right as it scrolls into view. +useScroll target ref, offset ['start end', 'center center']. +useTransform clipPath from 'inset(0% 100% 0% 0%)' to 'inset(0% 0% 0% 0%)'. +Also scale from 1.15 to 1. +``` + +```tsx +'use client'; +import { useRef } from 'react'; +import { motion, useScroll, useTransform } from 'motion/react'; + +export function ImageReveal({ src, alt }: { src: string; alt: string }) { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ['start end', 'center center'], + }); + + const clipPath = useTransform( + scrollYProgress, + [0, 1], + ['inset(0% 100% 0% 0%)', 'inset(0% 0% 0% 0%)'] + ); + const scale = useTransform(scrollYProgress, [0, 1], [1.15, 1]); + + return ( +
+ +
+ ); +} +``` + +--- + +### 6. Scroll-linked Navbar (Hide on Scroll Down) + +**Copilot Chat Prompt:** +``` +Framer Motion navbar: transparent when at top, white with shadow after 80px. +Hide by sliding up when scrolling down, reveal when scrolling up. +Use useScroll, useMotionValueEvent to detect direction. +Animate y, backgroundColor, boxShadow with motion.nav. +``` + +```tsx +'use client'; +import { useState } from 'react'; +import { motion, useScroll, useMotionValueEvent } from 'motion/react'; + +export function Navbar() { + const { scrollY } = useScroll(); + const [scrolled, setScrolled] = useState(false); + const [hidden, setHidden] = useState(false); + const [prev, setPrev] = useState(0); + + useMotionValueEvent(scrollY, 'change', latest => { + setScrolled(latest > 80); + setHidden(latest > prev && latest > 200); + setPrev(latest); + }); + + return ( + + ); +} +``` + +--- + +### 7. Staggered Card Grid + +**Copilot Chat Prompt:** +``` +Framer Motion: card grid with stagger entrance. Use variants: +container has staggerChildren 0.1, delayChildren 0.2. +Each card: hidden (opacity 0, y 40, scale 0.96) → visible (opacity 1, y 0, scale 1). +Trigger with whileInView on the container. Once. +``` + +```tsx +'use client'; +import { motion } from 'motion/react'; + +const containerVariants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.1, delayChildren: 0.2 } + } +}; + +const cardVariants = { + hidden: { opacity: 0, y: 40, scale: 0.96 }, + visible: { + opacity: 1, y: 0, scale: 1, + transition: { duration: 0.5, ease: [0.21, 0.47, 0.32, 0.98] } + } +}; + +export function CardGrid({ cards }: { cards: { id: number; title: string }[] }) { + return ( + + {cards.map(card => ( + +

{card.title}

+
+ ))} +
+ ); +} +``` + +--- + +### 8. 3D Tilt on Scroll + +**Copilot Chat Prompt:** +``` +Framer Motion: 3D perspective card that rotates on X axis as it scrolls through view. +rotateX 15→0→-15, scale 0.9→1→0.9, opacity 0→1→0. +Target ref with offset ['start end', 'end start']. Wrap in perspective container. +``` + +```tsx +'use client'; +import { useRef } from 'react'; +import { motion, useScroll, useTransform } from 'motion/react'; + +export function TiltCard({ children }: { children: React.ReactNode }) { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ['start end', 'end start'], + }); + + const rotateX = useTransform(scrollYProgress, [0, 0.5, 1], [15, 0, -15]); + const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.9, 1, 0.9]); + const opacity = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]); + + return ( +
+ + {children} + +
+ ); +} +``` + +--- + +## Variants Pattern for Stagger + +Variants propagate automatically from parent to children — you don't need to pass them down manually. + +```tsx +const parent = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.1, // Delay between each child + delayChildren: 0.2, // Initial delay before first child + when: 'beforeChildren', // Parent animates before children + } + } +}; + +const child = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } } +}; + +// Children with `variants={child}` automatically get the stagger +// when the parent transitions between 'hidden' and 'visible' +``` + +--- + +## Motion Value Events + +```tsx +import { useScroll, useMotionValueEvent } from 'motion/react'; + +const { scrollY } = useScroll(); + +// Fires on every change — use for imperative side effects +useMotionValueEvent(scrollY, 'change', latest => { + console.log('scroll position:', latest); +}); + +// Detect scroll direction +const [direction, setDirection] = useState<'up' | 'down'>('down'); + +useMotionValueEvent(scrollY, 'change', current => { + const diff = current - scrollY.getPrevious()!; + setDirection(diff > 0 ? 'down' : 'up'); +}); +``` + +**When to use `useMotionValueEvent` vs `useTransform`:** +- Use `useTransform` when you want a CSS value that animates smoothly (y, opacity, color) +- Use `useMotionValueEvent` when you want to fire React state changes or side effects + +--- + +## Next.js & App Router Notes + +```tsx +// Every file using motion hooks must be a Client Component +'use client'; + +// For page-level scroll tracking in App Router, use useScroll in a layout +// that's already a client component — don't try to use it in Server Components + +// If you need SSR-safe scroll animations, gate with: +import { useEffect, useState } from 'react'; +const [mounted, setMounted] = useState(false); +useEffect(() => setMounted(true), []); +if (!mounted) return null; // or a skeleton +``` + +**Recommended pattern for Next.js App Router:** +1. Keep all `motion.*` components in separate `'use client'` files +2. Import them into Server Components — they'll be client-rendered automatically +3. Use `AnimatePresence` at the layout level for page transitions + +--- + +## Accessibility + +```tsx +import { useReducedMotion } from 'motion/react'; + +export function AnimatedCard() { + const prefersReducedMotion = useReducedMotion(); + + return ( + + Content + + ); +} +``` + +Or disable all scroll-linked transforms when reduced motion is preferred: +```tsx +const prefersReducedMotion = useReducedMotion(); +const y = useTransform( + scrollYProgress, [0, 1], + prefersReducedMotion ? [0, 0] : [100, -100] // no movement if reduced motion +); +``` + +--- + +## Common Copilot Pitfalls + +**Missing 'use client':** Copilot forgets to add this for Next.js App Router files. +Every file using `useScroll`, `useTransform`, `motion.*`, or any hook needs `'use client'` at the top. + +**Using style prop on a plain div:** Copilot sometimes writes `
` where `y` is a MotionValue. +This silently does nothing. Must be ``. + +**Old import path:** Copilot still generates `from 'framer-motion'` (valid, but legacy). +Current canonical: `from 'motion/react'`. + +**Forgetting offset on useScroll:** Without `offset`, `scrollYProgress` tracks the full page +from 0 to 1 — not the element's position. Always pass `target` + `offset` for element-level tracking. + +**Missing ref on target:** Copilot sometimes writes `target: ref` but forgets to attach `ref` to the DOM element. +```tsx +const ref = useRef(null); +const { scrollYProgress } = useScroll({ target: ref }); // ← ref passed +return
...
; // ← ref attached +``` + +**Using animate prop for scroll-linked values:** Scroll-linked values must use `style`, not `animate`. +`animate` runs on mount/unmount, not on scroll. +```tsx +// ❌ Wrong + + +// ✅ Correct + +``` + +**Not smoothing scroll progress:** Raw `scrollYProgress` can feel mechanical on fine motion. +Wrap in `useSpring` for progress bars and UI elements that need a polished feel. diff --git a/skills/gsap-framer-scroll-animation/references/gsap.md b/skills/gsap-framer-scroll-animation/references/gsap.md new file mode 100644 index 000000000..6ba66f821 --- /dev/null +++ b/skills/gsap-framer-scroll-animation/references/gsap.md @@ -0,0 +1,685 @@ +# GSAP ScrollTrigger — Full Reference + +## Table of Contents +1. [Installation & Registration](#installation--registration) +2. [ScrollTrigger Config Reference](#scrolltrigger-config-reference) +3. [Start / End Syntax Decoded](#start--end-syntax-decoded) +4. [toggleActions Values](#toggleactions-values) +5. [Recipes with Copilot Prompts](#recipes-with-copilot-prompts) + - Fade-in batch reveal + - Scrub animation + - Pinned timeline + - Parallax layers + - Horizontal scroll + - Character stagger text + - Scroll snap + - Progress bar + - ScrollSmoother + - Scroll counter +6. [React Integration (useGSAP)](#react-integration-usegsap) +7. [Lenis Smooth Scroll](#lenis-smooth-scroll) +8. [Responsive with matchMedia](#responsive-with-matchmedia) +9. [Accessibility](#accessibility) +10. [Performance & Cleanup](#performance--cleanup) +11. [Common Copilot Pitfalls](#common-copilot-pitfalls) + +--- + +## Installation & Registration + +```bash +npm install gsap +# React +npm install gsap @gsap/react +``` + +```js +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { ScrollSmoother } from 'gsap/ScrollSmoother'; // optional +gsap.registerPlugin(ScrollTrigger, ScrollSmoother); +``` + +CDN (vanilla): +```html + + +``` + +--- + +## ScrollTrigger Config Reference + +```js +gsap.to('.element', { + x: 500, + ease: 'none', // Use 'none' for scrub animations + scrollTrigger: { + trigger: '.section', // Element whose position triggers the animation + start: 'top 80%', // "[trigger edge] [viewport edge]" + end: 'bottom 20%', // Where animation ends + scrub: true, // Link progress to scroll (true = instant) + scrub: 1, // Smooth scrub with 1s lag + pin: true, // Pin trigger element during scroll + pin: '.other-element', // Pin a different element + pinSpacing: true, // Add space below pinned element (default: true) + markers: true, // Debug markers — REMOVE in production + toggleActions: 'play none none reverse', // onEnter onLeave onEnterBack onLeaveBack + toggleClass: 'active', // CSS class added/removed when active + snap: 1, // Snap to nearest end position + snap: { snapTo: 'labels', duration: 0.3, ease: 'power1.inOut' }, + fastScrollEnd: true, // Force completion if user scrolls past fast + horizontal: false, // true for horizontal scroll containers + anticipatePin: 1, // Reduces pin jump (seconds to anticipate) + invalidateOnRefresh: true, // Recalculate positions on resize + id: 'my-trigger', // For ScrollTrigger.getById() + onEnter: () => {}, + onLeave: () => {}, + onEnterBack: () => {}, + onLeaveBack: () => {}, + onUpdate: self => console.log(self.progress), // 0 to 1 + onToggle: self => console.log(self.isActive), + } +}); +``` + +--- + +## Start / End Syntax Decoded + +Format: `"[trigger position] [viewport position]"` + +| Value | Meaning | +|---|---| +| `"top bottom"` | Top of trigger hits bottom of viewport — enters view | +| `"top 80%"` | Top of trigger reaches 80% down from top of viewport | +| `"top center"` | Top of trigger reaches viewport center | +| `"top top"` | Top of trigger at top of viewport | +| `"center center"` | Centers align | +| `"bottom top"` | Bottom of trigger at top of viewport — exits view | +| `"+=200"` | 200px after trigger position | +| `"-=100"` | 100px before trigger position | +| `"+=200%"` | 200% of viewport height after trigger | + +--- + +## toggleActions Values + +``` +toggleActions: "play pause resume reset" + ^ ^ ^ ^ + onEnter onLeave onEnterBack onLeaveBack +``` + +| Value | Effect | +|---|---| +| `play` | Play from current position | +| `pause` | Pause at current position | +| `resume` | Resume from where paused | +| `reverse` | Play backwards | +| `reset` | Jump to start | +| `restart` | Play from beginning | +| `none` | Do nothing | + +Most common for entrance animations: `"play none none none"` (animate once, don't reverse). + +--- + +## Recipes with Copilot Prompts + +### 1. Fade-in Batch Reveal + +**Copilot Chat Prompt:** +``` +Using GSAP ScrollTrigger.batch, animate all .card elements: +fade in from opacity 0, y 50 when they enter the viewport at 85%. +Stagger 0.15s between cards. Animate once (no reverse). +``` + +```js +gsap.registerPlugin(ScrollTrigger); + +ScrollTrigger.batch('.card', { + onEnter: elements => { + gsap.from(elements, { + opacity: 0, + y: 50, + stagger: 0.15, + duration: 0.8, + ease: 'power2.out', + }); + }, + start: 'top 85%', +}); +``` + +Why `batch` over individual ScrollTriggers: batch groups elements entering together into one animation call, which is more performant than creating one ScrollTrigger per element. + +--- + +### 2. Scrub Animation (scroll-linked) + +**Copilot Chat Prompt:** +``` +GSAP scrub: animate .hero-image scale from 1 to 1.3 and opacity to 0 +as the user scrolls past .hero-section. +Perfectly synced to scroll position, no pin. +``` + +```js +gsap.to('.hero-image', { + scale: 1.3, + opacity: 0, + ease: 'none', // Critical: linear easing for scrub + scrollTrigger: { + trigger: '.hero-section', + start: 'top top', + end: 'bottom top', + scrub: true, + } +}); +``` + +--- + +### 3. Pinned Timeline + +**Copilot Chat Prompt:** +``` +GSAP pinned timeline: pin .story-section while a sequence plays — +fade in .title (y: 60), scale .image to 1, slide .text from x: 80. +Total scroll distance 300vh. Scrub 1 for smoothness. +``` + +```js +const tl = gsap.timeline({ + scrollTrigger: { + trigger: '.story-section', + start: 'top top', + end: '+=300%', + pin: true, + scrub: 1, + anticipatePin: 1, + } +}); + +tl + .from('.title', { opacity: 0, y: 60, duration: 1 }) + .from('.image', { scale: 0.85, opacity: 0, duration: 1 }, '-=0.3') + .from('.text', { x: 80, opacity: 0, duration: 1 }, '-=0.3'); +``` + +--- + +### 4. Parallax Layers + +**Copilot Chat Prompt:** +``` +GSAP parallax: background image moves yPercent -20 (slow), +foreground text moves yPercent -60 (fast). Both scrubbed to scroll, no pin. +Trigger is .parallax-section, start top bottom, end bottom top. +``` + +```js +// Slow background +gsap.to('.parallax-bg', { + yPercent: -20, + ease: 'none', + scrollTrigger: { + trigger: '.parallax-section', + start: 'top bottom', + end: 'bottom top', + scrub: true, + } +}); + +// Fast foreground +gsap.to('.parallax-fg', { + yPercent: -60, + ease: 'none', + scrollTrigger: { + trigger: '.parallax-section', + start: 'top bottom', + end: 'bottom top', + scrub: true, + } +}); +``` + +--- + +### 5. Horizontal Scroll Section + +**Copilot Chat Prompt:** +``` +GSAP horizontal scroll: 4 .panel elements inside .panels-container. +Pin .horizontal-section, scrub 1, snap per panel. +End should use offsetWidth so it recalculates on resize. +``` + +```js +const sections = gsap.utils.toArray('.panel'); + +gsap.to(sections, { + xPercent: -100 * (sections.length - 1), + ease: 'none', + scrollTrigger: { + trigger: '.horizontal-section', + pin: true, + scrub: 1, + snap: 1 / (sections.length - 1), + end: () => `+=${document.querySelector('.panels-container').offsetWidth}`, + invalidateOnRefresh: true, + } +}); +``` + +Required HTML: +```html +
+
+
1
+
2
+
3
+
4
+
+
+``` + +Required CSS: +```css +.horizontal-section { overflow: hidden; } +.panels-container { display: flex; flex-wrap: nowrap; width: 400vw; } +.panel { width: 100vw; height: 100vh; flex-shrink: 0; } +``` + +--- + +### 6. Character Stagger Text Reveal + +**Copilot Chat Prompt:** +``` +Split .hero-title into characters using SplitType. +Animate each char: opacity 0→1, y 80→0, rotateX -90→0. +Stagger 0.03s, ease back.out(1.7). Trigger when heading enters at 85%. +``` + +```bash +npm install split-type +``` + +```js +import SplitType from 'split-type'; + +const text = new SplitType('.hero-title', { types: 'chars' }); + +gsap.from(text.chars, { + opacity: 0, + y: 80, + rotateX: -90, + stagger: 0.03, + duration: 0.6, + ease: 'back.out(1.7)', + scrollTrigger: { + trigger: '.hero-title', + start: 'top 85%', + toggleActions: 'play none none none', + } +}); +``` + +--- + +### 7. Scroll Snap Sections + +**Copilot Chat Prompt:** +``` +GSAP: each full-height section scales from 0.9 to 1 when it enters view. +Also add global scroll snapping between sections using ScrollTrigger.create snap. +``` + +```js +const sections = gsap.utils.toArray('section'); + +sections.forEach(section => { + gsap.from(section, { + scale: 0.9, + opacity: 0.6, + scrollTrigger: { + trigger: section, + start: 'top 90%', + toggleActions: 'play none none reverse', + } + }); +}); + +ScrollTrigger.create({ + snap: { + snapTo: (progress) => { + const step = 1 / (sections.length - 1); + return Math.round(progress / step) * step; + }, + duration: { min: 0.2, max: 0.5 }, + ease: 'power1.inOut', + } +}); +``` + +--- + +### 8. Scroll Progress Bar + +**Copilot Chat Prompt:** +``` +GSAP: fixed progress bar at top of page. scaleX 0→1 linked to +full page scroll, scrub 0.3 for slight smoothing. transformOrigin left center. +``` + +```js +gsap.to('.progress-bar', { + scaleX: 1, + ease: 'none', + transformOrigin: 'left center', + scrollTrigger: { + trigger: document.body, + start: 'top top', + end: 'bottom bottom', + scrub: 0.3, + } +}); +``` + +```css +.progress-bar { + position: fixed; top: 0; left: 0; + width: 100%; height: 4px; + background: #6366f1; + transform-origin: left; + transform: scaleX(0); + z-index: 999; +} +``` + +--- + +### 9. ScrollSmoother Setup + +**Copilot Chat Prompt:** +``` +Set up GSAP ScrollSmoother with smooth: 1.5, effects: true. +Show the required wrapper HTML structure. +Add data-speed and data-lag to parallax elements. +``` + +```bash +# ScrollSmoother is part of gsap — no extra install needed +``` + +```js +import { ScrollSmoother } from 'gsap/ScrollSmoother'; +gsap.registerPlugin(ScrollTrigger, ScrollSmoother); + +ScrollSmoother.create({ + wrapper: '#smooth-wrapper', + content: '#smooth-content', + smooth: 1.5, + effects: true, + smoothTouch: 0.1, +}); +``` + +```html +
+
+ +
...
+
+
+``` + +--- + +### 10. Animated Number Counter + +**Copilot Chat Prompt:** +``` +GSAP: animate .counter elements from 0 to their data-target value +when they enter the viewport. Duration 2s, ease power2.out. +Format with toLocaleString. Animate once. +``` + +```js +document.querySelectorAll('.counter').forEach(el => { + const obj = { val: 0 }; + gsap.to(obj, { + val: parseInt(el.dataset.target, 10), + duration: 2, + ease: 'power2.out', + onUpdate: () => { el.textContent = Math.round(obj.val).toLocaleString(); }, + scrollTrigger: { + trigger: el, + start: 'top 85%', + toggleActions: 'play none none none', + } + }); +}); +``` + +```html +0 +``` + +--- + +## React Integration (useGSAP) + +```bash +npm install gsap @gsap/react +``` + +```jsx +import { useRef } from 'react'; +import { useGSAP } from '@gsap/react'; +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +gsap.registerPlugin(useGSAP, ScrollTrigger); +``` + +**Why useGSAP instead of useEffect:** +`useGSAP` automatically kills all ScrollTriggers created inside it when the component unmounts — preventing memory leaks. It also handles React strict mode's double-invoke correctly. Think of it as a drop-in replacement for `useLayoutEffect` that GSAP understands. + +**Copilot Chat Prompt:** +``` +React: use useGSAP from @gsap/react to animate .card elements inside containerRef. +Fade in from y 60, opacity 0, stagger 0.12, scrollTrigger start top 80%. +Scope to containerRef so selectors don't match outside this component. +``` + +```jsx +export function AnimatedSection() { + const containerRef = useRef(null); + + useGSAP(() => { + gsap.from('.card', { + opacity: 0, + y: 60, + stagger: 0.12, + duration: 0.7, + ease: 'power2.out', + scrollTrigger: { + trigger: containerRef.current, + start: 'top 80%', + toggleActions: 'play none none none', + } + }); + }, { scope: containerRef }); + + return ( +
+
One
+
Two
+
+ ); +} +``` + +**Pinned timeline in React:** +```jsx +export function PinnedStory() { + const sectionRef = useRef(null); + + useGSAP(() => { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: sectionRef.current, + pin: true, scrub: 1, + start: 'top top', end: '+=200%', + } + }); + tl.from('.story-title', { opacity: 0, y: 40 }) + .from('.story-image', { scale: 0.85, opacity: 0 }, '-=0.2') + .from('.story-text', { opacity: 0, x: 40 }, '-=0.2'); + }, { scope: sectionRef }); + + return ( +
+

Chapter One

+ +

The story begins.

+
+ ); +} +``` + +**Next.js note:** Run `gsap.registerPlugin(ScrollTrigger)` inside a `useGSAP` or `useLayoutEffect` — or guard it: +```js +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); +``` + +--- + +## Lenis Smooth Scroll + +```bash +npm install lenis +``` + +**Copilot Chat Prompt:** +``` +Integrate Lenis smooth scroll with GSAP ScrollTrigger. +Add lenis.raf to gsap.ticker. Set lagSmoothing to 0. +Destroy lenis on unmount if in React. +``` + +```js +import Lenis from 'lenis'; + +const lenis = new Lenis({ duration: 1.2, smoothWheel: true }); + +gsap.ticker.add(time => lenis.raf(time * 1000)); +gsap.ticker.lagSmoothing(0); +lenis.on('scroll', ScrollTrigger.update); + +// React cleanup +useEffect(() => { + return () => { + lenis.destroy(); + gsap.ticker.remove(lenis.raf); + }; +}, []); +``` + +--- + +## Responsive with matchMedia + +**Copilot Chat Prompt:** +``` +Use gsap.matchMedia to animate x: 200 on desktop (min-width: 768px) +and y: 100 on mobile. Both should skip animation if prefers-reduced-motion is set. +``` + +```js +const mm = gsap.matchMedia(); + +mm.add({ + isDesktop: '(min-width: 768px)', + isMobile: '(max-width: 767px)', + noMotion: '(prefers-reduced-motion: reduce)', +}, context => { + const { isDesktop, isMobile, noMotion } = context.conditions; + if (noMotion) return; + + gsap.from('.box', { + x: isDesktop ? 200 : 0, + y: isMobile ? 100 : 0, + opacity: 0, + scrollTrigger: { trigger: '.box', start: 'top 80%' } + }); +}); +``` + +--- + +## Accessibility + +```js +// Guard all scroll animations with prefers-reduced-motion +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + +if (!prefersReducedMotion.matches) { + gsap.from('.box', { + opacity: 0, y: 50, + scrollTrigger: { trigger: '.box', start: 'top 85%' } + }); +} else { + // Show element immediately, no animation + gsap.set('.box', { opacity: 1, y: 0 }); +} +``` + +Or use `gsap.matchMedia()` with `prefers-reduced-motion: reduce` condition (see above). + +--- + +## Performance & Cleanup + +```js +// Kill a specific trigger +const st = ScrollTrigger.create({ ... }); +st.kill(); + +// Kill all triggers (e.g., on page transition) +ScrollTrigger.killAll(); + +// Refresh all trigger positions (after dynamic content loads) +ScrollTrigger.refresh(); +``` + +**Performance rules:** +- Only animate `transform` and `opacity` — GPU-accelerated, no layout recalculation +- Avoid animating `width`, `height`, `top`, `left`, `box-shadow`, `filter` +- Use `ScrollTrigger.batch()` for many similar elements — far better than one trigger per element +- Add `will-change: transform` sparingly — only on actively animating elements +- Always remove `markers: true` before production + +--- + +## Common Copilot Pitfalls + +**Forgot registerPlugin:** Copilot often omits `gsap.registerPlugin(ScrollTrigger)`. +Always add it before any ScrollTrigger usage. + +**Wrong ease for scrub:** Copilot defaults to `power2.out` even on scrub animations. +Always use `ease: 'none'` when `scrub: true` or `scrub: number`. + +**useEffect instead of useGSAP in React:** Copilot generates `useEffect` — always swap to `useGSAP`. + +**Static end value for horizontal scroll:** Copilot writes `end: "+=" + container.offsetWidth`. +Correct: `end: () => "+=" + container.offsetWidth` (function form recalculates on resize). + +**markers left in production:** Copilot adds `markers: true` and leaves it. Always remove. + +**Scrub without pin on long animations:** Scrubbing a long timeline without pinning means +the element scrolls out of view. Add `pin: true` or shorten the scroll distance. From 85c1e078fa83b46c3fa3d0d0c13d6a325c44bcaa Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 3 Apr 2026 21:17:31 +0530 Subject: [PATCH 2/2] fix: address review comments for gsap-framer-scroll-animation skill --- docs/README.skills.md | 2 +- skills/gsap-framer-scroll-animation/SKILL.md | 18 +++++++++++++----- .../references/framer.md | 13 +++++++------ .../references/gsap.md | 15 +++++++-------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/docs/README.skills.md b/docs/README.skills.md index 7eeb21707..ae8145fec 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -152,7 +152,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [github-copilot-starter](../skills/github-copilot-starter/SKILL.md) | Set up complete GitHub Copilot configuration for a new project based on technology stack | None | | [github-issues](../skills/github-issues/SKILL.md) | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, set issue fields (dates, priority, custom fields), set issue types, manage issue workflows, link issues, add dependencies, or track blocked-by/blocking relationships. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", "set the priority", "set the start date", "link issues", "add dependency", "blocked by", "blocking", or any GitHub issue management task. | `references/dependencies.md`
`references/images.md`
`references/issue-fields.md`
`references/issue-types.md`
`references/projects.md`
`references/search.md`
`references/sub-issues.md`
`references/templates.md` | | [go-mcp-server-generator](../skills/go-mcp-server-generator/SKILL.md) | Generate a complete Go MCP server project with proper structure, dependencies, and implementation using the official github.com/modelcontextprotocol/go-sdk. | None | -| [gsap-framer-scroll-animation](../skills/gsap-framer-scroll-animation/SKILL.md) | 'Use this skill whenever the user wants to build scroll animations, scroll effects, parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, or any motion tied to scroll position — in vanilla JS, React, or Next.js. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, useSpring, whileInView, variants). Use this skill even if the user just says "animate on scroll", "fade in as I scroll", "make it scroll like Apple", "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish.' | `references/framer.md`
`references/gsap.md` | +| [gsap-framer-scroll-animation](../skills/gsap-framer-scroll-animation/SKILL.md) | Use this skill whenever the user wants to build scroll animations, scroll effects, parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, or any motion tied to scroll position — in vanilla JS, React, or Next.js. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, ScrollSmoother, matchMedia) and Framer Motion / Motion v12 (useScroll, useTransform, useSpring, whileInView, variants). Use this skill even if the user just says "animate on scroll", "fade in as I scroll", "make it scroll like Apple", "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish. | `references/framer.md`
`references/gsap.md` | | [gtm-0-to-1-launch](../skills/gtm-0-to-1-launch/SKILL.md) | Launch new products from idea to first customers. Use when launching products, finding early adopters, building launch week playbooks, diagnosing why adoption stalls, or learning that press coverage does not equal growth. Includes the three-layer diagnosis, the 2-week experiment cycle, and the launch that got 50K impressions and 12 signups. | None | | [gtm-ai-gtm](../skills/gtm-ai-gtm/SKILL.md) | Go-to-market strategy for AI products. Use when positioning AI products, handling "who is responsible when it breaks" objections, pricing variable-cost AI, choosing between copilot/agent/teammate framing, or selling autonomous tools into enterprises. | None | | [gtm-board-and-investor-communication](../skills/gtm-board-and-investor-communication/SKILL.md) | Board meeting preparation, investor updates, and executive communication. Use when preparing board decks, writing investor updates, handling bad news with the board, structuring QBRs, or building board-level metric discipline. Includes the "Three Things" narrative model, the 4-tier metric hierarchy, and the pre-brief pattern that prevents board surprises. | None | diff --git a/skills/gsap-framer-scroll-animation/SKILL.md b/skills/gsap-framer-scroll-animation/SKILL.md index 92e6f1b5a..4ac7013c3 100644 --- a/skills/gsap-framer-scroll-animation/SKILL.md +++ b/skills/gsap-framer-scroll-animation/SKILL.md @@ -1,7 +1,7 @@ --- name: gsap-framer-scroll-animation description: >- - 'Use this skill whenever the user wants to build scroll animations, scroll effects, + Use this skill whenever the user wants to build scroll animations, scroll effects, parallax, scroll-triggered reveals, pinned sections, horizontal scroll, text animations, or any motion tied to scroll position — in vanilla JS, React, or Next.js. Covers GSAP ScrollTrigger (pinning, scrubbing, snapping, timelines, horizontal scroll, @@ -10,7 +10,7 @@ description: >- "animate on scroll", "fade in as I scroll", "make it scroll like Apple", "parallax effect", "sticky section", "scroll progress bar", or "entrance animation". Also triggers for Copilot prompt patterns for GSAP or Framer Motion code generation. - Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish.' + Pairs with the premium-frontend-ui skill for creative philosophy and design-level polish. --- # GSAP & Framer Motion — Scroll Animations Skill @@ -19,7 +19,7 @@ Production-grade scroll animations with GitHub Copilot prompts, ready-to-use cod > **Design Companion:** This skill provides the *technical implementation* for scroll-driven motion. > For the *creative philosophy*, design principles, and premium aesthetics that should guide **how** -> and **when** to animate, always cross-reference the [premium-frontend-ui](../premium-frontend-ui/SKILL.md) skill. +> and **when** to animate, always cross-reference the **premium-frontend-ui** skill. > Together they form a complete approach: premium-frontend-ui decides the **what** and **why**; > this skill delivers the **how**. @@ -60,6 +60,14 @@ import { motion, useScroll, useTransform, useSpring } from 'motion/react'; // legacy: import { motion } from 'framer-motion' — also valid ``` +## Workflow + +1. Interpret the user's intent to identify if GSAP or Framer Motion is the best fit. +2. Read the relevant reference document in `references/` for detailed APIs and patterns. +3. Suggest the required package installation if not already present. +4. Implement the scaffold for the animation structure, adhering to the requested format (React components, hook requirements, or vanilla JS). +5. Apply the correct tools (scrolling vs in-view elements) ensuring accessibility options are present and hooks don't cause infinite re-renders. + ## The 5 Most Common Scroll Patterns Quick reference — full recipes with Copilot prompts are in the reference files. @@ -115,7 +123,7 @@ tl.from('.title', { opacity: 0, y: 60 }).from('.img', { scale: 0.85 }); - **Framer Next.js**: always add `'use client'` at top of any file using motion hooks - **Both**: animate only `transform` and `opacity` — avoid `width`, `height`, `box-shadow` - **Accessibility**: always check `prefers-reduced-motion` — see each reference file for patterns -- **Premium polish**: follow the [premium-frontend-ui](../premium-frontend-ui/SKILL.md) principles for motion timing, easing curves, and restraint — animation should enhance, never overwhelm +- **Premium polish**: follow the **premium-frontend-ui** skill principles for motion timing, easing curves, and restraint — animation should enhance, never overwhelm ## Copilot Prompting Tips @@ -136,4 +144,4 @@ tl.from('.title', { opacity: 0, y: 60 }).from('.img', { scale: 0.85 }); | Skill | Relationship | |---|---| -| [premium-frontend-ui](../premium-frontend-ui/SKILL.md) | Creative philosophy, design principles, and aesthetic guidelines — defines *when* and *why* to animate | +| **premium-frontend-ui** | Creative philosophy, design principles, and aesthetic guidelines — defines *when* and *why* to animate | diff --git a/skills/gsap-framer-scroll-animation/references/framer.md b/skills/gsap-framer-scroll-animation/references/framer.md index a62b600ab..5fa282a8d 100644 --- a/skills/gsap-framer-scroll-animation/references/framer.md +++ b/skills/gsap-framer-scroll-animation/references/framer.md @@ -282,7 +282,6 @@ export function ParallaxSection() { return (
@@ -406,19 +405,21 @@ Animate y, backgroundColor, boxShadow with motion.nav. ```tsx 'use client'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { motion, useScroll, useMotionValueEvent } from 'motion/react'; export function Navbar() { const { scrollY } = useScroll(); const [scrolled, setScrolled] = useState(false); const [hidden, setHidden] = useState(false); - const [prev, setPrev] = useState(0); + const prevRef = useRef(0); useMotionValueEvent(scrollY, 'change', latest => { - setScrolled(latest > 80); - setHidden(latest > prev && latest > 200); - setPrev(latest); + const nextScrolled = latest > 80; + const nextHidden = latest > prevRef.current && latest > 200; + setScrolled(current => (current === nextScrolled ? current : nextScrolled)); + setHidden(current => (current === nextHidden ? current : nextHidden)); + prevRef.current = latest; }); return ( diff --git a/skills/gsap-framer-scroll-animation/references/gsap.md b/skills/gsap-framer-scroll-animation/references/gsap.md index 6ba66f821..0591d6aad 100644 --- a/skills/gsap-framer-scroll-animation/references/gsap.md +++ b/skills/gsap-framer-scroll-animation/references/gsap.md @@ -58,16 +58,13 @@ gsap.to('.element', { trigger: '.section', // Element whose position triggers the animation start: 'top 80%', // "[trigger edge] [viewport edge]" end: 'bottom 20%', // Where animation ends - scrub: true, // Link progress to scroll (true = instant) - scrub: 1, // Smooth scrub with 1s lag - pin: true, // Pin trigger element during scroll - pin: '.other-element', // Pin a different element + scrub: 1, // Link progress to scroll; use true for instant scrub, or a number for smooth lag + pin: true, // Pin trigger element during scroll; use a selector/element to pin something else pinSpacing: true, // Add space below pinned element (default: true) markers: true, // Debug markers — REMOVE in production toggleActions: 'play none none reverse', // onEnter onLeave onEnterBack onLeaveBack toggleClass: 'active', // CSS class added/removed when active - snap: 1, // Snap to nearest end position - snap: { snapTo: 'labels', duration: 0.3, ease: 'power1.inOut' }, + snap: { snapTo: 'labels', duration: 0.3, ease: 'power1.inOut' }, // Or use a number like 1 to snap to increments fastScrollEnd: true, // Force completion if user scrolls past fast horizontal: false, // true for horizontal scroll containers anticipatePin: 1, // Reduces pin jump (seconds to anticipate) @@ -574,10 +571,12 @@ Destroy lenis on unmount if in React. ```js import Lenis from 'lenis'; +import { useEffect } from 'react'; const lenis = new Lenis({ duration: 1.2, smoothWheel: true }); -gsap.ticker.add(time => lenis.raf(time * 1000)); +const raf = (time) => lenis.raf(time * 1000); +gsap.ticker.add(raf); gsap.ticker.lagSmoothing(0); lenis.on('scroll', ScrollTrigger.update); @@ -585,7 +584,7 @@ lenis.on('scroll', ScrollTrigger.update); useEffect(() => { return () => { lenis.destroy(); - gsap.ticker.remove(lenis.raf); + gsap.ticker.remove(raf); }; }, []); ```