---
name: madebymustafa-taste-gsap
description: "GSAP implementation craft for agent-built sites: timeline architecture, gsap.context and matchMedia, ScrollTrigger pin and scrub hygiene, SplitText discipline, transform-only animation, refresh-after-fonts, and full prefers-reduced-motion gating. The 90% of award-tier motion is structure, not effects — this is that 90%. Use whenever generating GSAP animation code. Stack on the design-taste core skill and taste-motion."
license: "MIT"
metadata:
  decimalai-skill-id: "d7bd9eae-e60a-4609-ae40-2808c69e9477"
  decimalai-version-id: "7c23ccdf-c827-43b4-9fe8-87e1489c95d3"
  decimalai-version: "1"
  decimalai-body-sha256: "5be0785a44792831f8e585e3f87e236a725f865e7ed8c7d31cc960896a58f4d2"
  decimalai-name: "madebymustafa/taste-gsap"
  decimalai-format: "1"
  decimalai-invocation: "model"
  decimalai-category: "frontend"
  decimalai-source: "https://app.decimal.ai/s/madebymustafa-taste-gsap@1/SKILL.md"
---
# taste-gsap

> GSAP is a scalpel, not a glitter gun. One clean pinned section beats nine parallax layers — and survives a code review.

## 1. ARCHITECTURE (The Non-Negotiable Shell)

- Wrap every page's animations in a single `gsap.context(() => { ... }, rootElement)` — it scopes selectors and gives you instant teardown.
- Gate everything through `gsap.matchMedia()`:
  - `(prefers-reduced-motion: no-preference)` — all animation lives here. Reduced-motion users get the static, complete page.
  - `(min-width: 768px)` — heavy effects (pins, scrubs, per-character splits) desktop-only; small screens get fades or nothing.
- The matchMedia callback returns a cleanup function; on SPAs, call `ctx.revert()` on unmount.
- One timeline per section, named by section; never one giant global timeline with 40 tweens.
- Lenis (free, not ScrollSmoother): `new Lenis({ duration: ~1.1, anchors: true })`, pipe it to ScrollTrigger with `lenis.on("scroll", ScrollTrigger.update)` + `gsap.ticker.add(t => lenis.raf(t * 1000))` + `gsap.ticker.lagSmoothing(0)`. `lenis.stop()` during the preloader, `lenis.start()` after. Never smooth-scroll under reduced motion.

## 2. PIN & SCRUB HYGIENE (Where Agents Fail Loudest)

- **Max two pinned sections per page.** Every pin costs the scroll thread. Two is the budget; one is the norm.
- Pins: `pin: true, pinSpacing: true` (default — keep it), `start: "top top"`, `end: "+=200%"` to `"+=300%"`. Longer ends = slower scrub = more jank on mobile.
- Horizontal galleries (the signature Awwwards move): pin the *section*, translate the *track* (`x` only, never `left`), `end: () => "+=" + (track.scrollWidth - innerWidth)` so the distance recalculates on resize, `scrub: 1`, desktop-only. Native `overflow-x` + scroll-snap must remain as the no-JS / reduced-motion fallback.
- Drag carousels (Magma-school): `Draggable.create(track, { type: "x", inertia: true, bounds: () => ({ minX: -(track.scrollWidth - wrap.offsetWidth), maxX: 0 }) })` — bounds as a *function* so it recalibrates on resize. `edgeResistance` ~0.9, native `overflow-x` + snap as the no-JS fallback, and a cursor/UI hint that the strip is draggable.
- `scrub: 0.6`–`1` on heavy sections; `scrub: true` only on trivial tweens. A scrubbed tween without smoothing fights the scroll.
- Never pin inside another pin; never start a second pin before the first's spacer ends. `ScrollTrigger.refresh()` after fonts/images (`window.load` + `document.fonts.ready`) or everything misaligns.
- Test pins at 768px width before shipping — pinned sections that trap mobile users are a bug, not a feature (this is why pins live in the `(min-width: 768px)` gate).
- `invalidateOnRefresh: true` on refresh-dependent triggers so re-layouts don't drift.

## 3. WHAT TO ANIMATE (Transform and Opacity Only)

- Animate `x`, `y`, `scale`, `rotation`, `opacity` — never `top/left/width/height/margin/filter` (layout thrash = jank).
- Text reveals: line-level splits (`overflow: hidden` wrapper + `yPercent: 110` → 0) by default. Character-level only for hero headlines, and only inside the desktop gate.
- No `will-change` spam — set it on the animated element's wrapper while animating, remove after (or let GSAP manage it). Every `will-change` is a memory promise.
- Keep 60fps honesty: no 200-element stagger in one frame, no scrubbing 40 DOM properties. Batch with `gsap.utils.toArray` and stagger — one tween, many targets, cheap.

## 4. REVEAL PATTERNS (The Professional's Defaults)

- **From-states stay in code, not CSS:** elements start visible in CSS; `gsap.from(...)` with `immediateRender: true` inside the matchMedia gate hides them only when JS + motion are active. If GSAP never loads, the page is complete — this is the safety contract.
- Standard reveal: `opacity: 0, y: 40, duration: 0.9, ease: "power3.out"`, trigger `top 82%`, `once: true` for content sections.
- Hero intro: a `gsap.timeline()` on load — 80–120ms stagger between elements, `ease: "expo.out"` or `power4.out`, total under 1.4s. The page is a story with a first line, not a slideshow.
- Count-ups: tween a proxy object (`gsap.to(obj, { v: target })`), write `el.textContent` in `onUpdate`, `snap` optional. `once: true` on the trigger.

## 5. THE FULL STACK OF SINS (Each Is a Fail)

- Scroll-jacking the wheel without a smooth-scroll layer (Lenis) and then not testing it — native scroll is a feature.
- Animations that trigger before the content exists (missing `ScrollTrigger.refresh()`).
- Infinite marquees that aren't paused under reduced motion — kill them in CSS with `@media (prefers-reduced-motion: reduce)` too, not just in JS.
- Autoplaying anything: hero video, accordions, carousels that move without input. Awwwards confidence is stillness between moments.
- A page whose "wow" depends on animation: if the still screenshot is boring, the animation is makeup, not design.

## 6. CHECKLIST (Fail the Animation If…)

- [ ] No `gsap.context` + `matchMedia` gating (reduced-motion users see animation).
- [ ] More than two pinned sections, or a pin under 768px.
- [ ] Any animated layout property (`top/left/width/height`).
- [ ] No `ScrollTrigger.refresh()` after fonts/images.
- [ ] Per-character splits outside the desktop gate.
- [ ] Marquee or autoplay without a reduced-motion kill switch.
- [ ] The still frame (no JS) isn't a complete, beautiful page.
- [ ] Any animation that exists for its own sake — every move serves the story or dies.
