Generate on-page animations for React/Next.js marketing sites using a CSS-first, Motion-surgical approach. Use when creating entrance animations, scroll-triggered reveals, product demos, interactive components, hero sections, or any animated UI element. Also use when asked to add animation, motion, or transitions to a page or component. Triggers on "animate", "animation", "add motion", "scroll animation", "hero animation", "product demo animation", "entrance effect", "fade in", "parallax".
From gtmnpx claudepluginhub inkeep/team-skills --plugin gtmThis skill uses the workspace's default tool permissions.
references/css-patterns.mdreferences/decision-framework.mdreferences/inspiration-repos.mdreferences/motion-patterns.mdreferences/performance-rules.mdreferences/product-demo-patterns.mdGuides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Configures VPN and dedicated connections like Direct Connect, ExpressRoute, Interconnect for secure on-premises to AWS, Azure, GCP, OCI hybrid networking.
Generate production-quality on-page animations for React + Next.js + Tailwind sites. The core principle is CSS-first, Motion-surgical — use pure CSS and IntersectionObserver for 80-90% of animations, reach for Motion (Framer Motion) only when CSS genuinely cannot solve the problem.
Why CSS-first:
"use client" boundary required (preserves React Server Components)Motion is irreplaceable for exactly 6 patterns:
AnimatePresence)layoutId)height: auto (accordion expand/collapse) — CSS cannot interpolate to height: auto. The emerging interpolate-size: allow-keywords (Chrome 129+) will solve this but is not yet Baseline.If the animation doesn't require one of these five, use CSS.
Before starting any work, create a task for each step using TaskCreate with addBlockedBy to enforce ordering. Derive descriptions and completion criteria from each step's own workflow text.
Mark each task in_progress when starting and completed when its step's exit criteria are met. On re-entry, check TaskList first and resume from the first non-completed task.
Identify:
Walk through this flowchart. Stop at the first YES.
Is the animation purely CSS-driven (hover, focus, keyframes, scroll-timeline)?
├── YES → Tier 1: Pure CSS
│ Load: references/css-patterns.md
└── NO
├── Does the element need to animate on React unmount?
│ └── YES → Tier 4: Motion (AnimatePresence)
│ Load: references/motion-patterns.md
├── Does the element need to animate to a new DOM position?
│ └── YES → Tier 4: Motion (layoutId)
│ Load: references/motion-patterns.md
├── Does the user interact via drag/swipe/pinch?
│ └── YES → Tier 4: Motion (gestures)
│ Load: references/motion-patterns.md
├── Does the animation need scroll-trigger visibility detection?
│ └── YES → Tier 2: CSS + IntersectionObserver
│ Load: references/css-patterns.md
└── Is the animation a simple entrance triggered by mount?
└── YES → Tier 2: CSS + IntersectionObserver
Load: references/css-patterns.md
When in doubt, default to Tier 2 (CSS + IntersectionObserver). This covers the vast majority of marketing site animations.
If the request is specifically a product demo animation (showing software UI in action, cursor movements, screen transitions), also Load: references/product-demo-patterns.md
If the built-in patterns don't cover the requested animation type, Load: references/inspiration-repos.md — OSS repos with inspectable source code for unusual effects.
Before generating code, scan the target codebase for existing animation patterns:
Check for Motion usage: grep -r "from.*motion" src/ --include="*.tsx" | head -5
whileInView fade-ups are candidates for CSS replacementCheck for existing easing/duration conventions:
ease: or transition: patterns in existing animated componentsease-out (CSS) or [0.25, 0.46, 0.45, 0.94] (Motion cubic-bezier)500ms for fade-ups, 300ms for hover/micro-interactions, 600-800ms for hero reveals150-300ms per Nielsen Norman Group and Material Design guidelines. The 500ms entrance default is for scroll-triggered reveals seen once, not repeated interactions.80ms between items. Total stagger time should stay under 500ms — scale per-item delay inversely with item count: 80ms for 4 items, 50ms for 8 items, 25ms for 16+ items.20-30px upward (translateY(20px) → translateY(0)) — use fixed pixels for fade-up entrances (visual consistency across element sizes). Use translateY(100%) (percentages) only for off-screen positioning (toasts, drawers, sheets sliding fully in/out).Check Tailwind version. Tailwind v4 has breaking changes for animations:
transition-[opacity,transform] does NOT work — v4 uses individual properties. Use transition-[opacity,translate,scale,rotate] instead.blur-sm is 8px in v4 (was 4px in v3). Use blur-xs for the old blur-sm behavior.perspective-*, rotate-x-*, rotate-y-*, translate-z-*, transform-3dstarting: variant (maps to @starting-style) and transition-discrete (maps to transition-behavior: allow-discrete)@theme in CSS instead of tailwind.config.js:
@theme {
--animate-fade-up: fade-up 0.5s ease-out;
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(20px); }
}
Load the appropriate reference file and generate the component. Follow these rules for ALL output:
Load: references/performance-rules.md — always, for every animation.
Component conventions:
"use client" only if the component uses React hooks or Motion. CSS-only animations with no JS interaction do NOT need it.transition-all, duration-500, ease-out). Fall back to inline styles or @keyframes in a <style> tag or CSS module for complex sequences.react-intersection-observer if already in the project, otherwise use the native API with a custom hook.Before delivering, check:
prefers-reduced-motion handled via no-motion-first pattern (spatial motion opt-in; opacity/color transitions always active)"use client" added only when necessaryonce: true (don't replay on every scroll)| Animation needed | Tier | Approach |
|---|---|---|
| Fade-up on scroll | 2 | CSS transition + IntersectionObserver class toggle |
| Hover card lift | 1 | CSS transition + :hover pseudo-class |
| Staggered grid entrance | 2 | CSS animation-delay via custom property + IntersectionObserver |
| Hero perspective reveal | 1-2 | CSS @keyframes with rotateX + perspective container |
| SVG line draw | 1 | CSS stroke-dashoffset animation |
| Background gradient | 1 | CSS @property + @keyframes |
| Tab content swap with exit | 4 | Motion AnimatePresence mode="wait" |
| Animated tab indicator | 4 | Motion layoutId |
| Modal/dropdown close | 4 | Motion AnimatePresence |
| Drag-to-reorder | 4 | Motion drag + Reorder |
| Scroll-linked parallax | 1 | CSS animation-timeline: view() |
| Product demo (UI showcase) | 2 | CSS + IntersectionObserver + product demo patterns |
| Looping product demo | 2-4 | State machine with CSS transitions (or useAnimationFrame for complex sequences) |
| Clip-path reveal | 1 | CSS clip-path transition |
| Animated counter | 1 | CSS @property with <integer> syntax |
❌ Using motion.div for a simple fade-up entrance
✅ CSS class toggle via IntersectionObserver — avoids adding "use client" boundary and bundle cost
❌ Animating width, height, top, left, margin, padding
✅ Animating transform, opacity, filter, clip-path — compositor-safe
❌ Missing prefers-reduced-motion handling
✅ No-motion-first: spatial motion inside @media (prefers-reduced-motion: no-preference); opacity/color always active
❌ Global * { animation-duration: 0.01ms !important } — kills helpful opacity/color transitions, prevents per-component overrides, can cause JS animation libraries to complete instantly
✅ Each component controls its own reduced-motion behavior — keep opacity fades, remove transform-based movement
❌ Forgetting "use client" on a component that uses Motion hooks
✅ Add "use client" at the top when using motion/react imports
❌ Adding "use client" to a component that only uses CSS animations
✅ CSS animations work in Server Components — no "use client" needed
❌ Entrance animations that replay every time element scrolls in/out of view
✅ Use triggerOnce: true (react-intersection-observer) or observe once then disconnect
❌ AnimatePresence wrapping an element that's always in the DOM
✅ AnimatePresence is only for elements that mount/unmount ({show && <Component />}).
For class-toggle visibility, use CSS transition-behavior: allow-discrete instead.
❌ Using Motion for scroll-linked parallax when page has no other Motion usage
✅ CSS animation-timeline: view() with @supports fallback — runs on compositor thread
❌ Wrapping an entire page component in "use client" just for one animation
✅ Extract only the animated wrapper as a Client Component; pass content as children
❌ Using Motion for animation that coexists with heavy JS work (route changes, data loading)
✅ Use CSS transitions — they run on the compositor thread and are unaffected by main thread load.
Documented production issue: Vercel replaced Motion shared layout animations with CSS because
Motion dropped frames during page transitions when the main thread was busy loading the new page.
❌ Defining the same @keyframes in multiple CSS files across the project
✅ Define animation tokens (duration, easing, keyframes) once in globals.css, reference via custom properties
❌ Putting <LazyMotion> at the app root level
✅ Wrap LazyMotion around only the specific component that needs Motion — root-level causes subtree re-renders
/motion-video, /video-pipeline, /blog-to-video) — same visual concept, rendered to MP4 for social distribution./graphics — Figma designs, AI-generated images, 3D renders.@react-three/fiber) for WebGL content, or Rive for interactive 2D/2.5D.