Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Advanced motion and animation design skill. Covers scroll-storytelling, physics-grade interactions, Three.js/WebGL, GSAP choreography, and cinematic page transitions. Use alongside forge-core when MOTION_DEPTH >= 8 is needed.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 47% | 0% |
> Motion is not decoration. It is communication. > Every animation must answer: what does this movement mean to the user?
CHOREOGRAPHY: 8 (1=Simple fade / 10=Hollywood-grade sequencing)
PHYSICS_GRADE: 8 (1=Linear ease / 10=Mass, friction, spring simulation)
PERFORMANCE_CAP: 9 (1=Any GPU cost / 10=60fps on mid-range Android)Before adding any animation, answer:
prefers-reduced-motion?If #2 is "no" and #3 is "no" → remove the animation.
prefers-reduced-motionEvery animation MUST respect the user's system preference:
css@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }
In Framer Motion: check useReducedMotion() and substitute with instant opacity changes.
The spring is the universal language of premium motion. Use these configurations:
js// Definitions const springs = { // Heavy, authoritative — modals, drawers heavy: { type: 'spring', stiffness: 80, damping: 18, mass: 1.2 }, // Standard UI — buttons, cards, list items default: { type: 'spring', stiffness: 140, damping: 20, mass: 0.8 }, // Snappy, direct — tooltips, badges, micro-interactions snappy: { type: 'spring', stiffness: 300, damping: 28, mass: 0.5 }, // Gentle, atmospheric — hero reveals, page entries drift: { type: 'spring', stiffness: 60, damping: 15, mass: 1.0 }, // Aggressive overshoot — celebration, success confirmations bounce: { type: 'spring', stiffness: 400, damping: 12, mass: 0.6 }, };
Rule: Never use ease, ease-in-out, or linear for interactive elements. Springs only. CSS transitions on non-interactive ambient elements may use cubic-bezier(0.16, 1, 0.3, 1).
jsxconst pageVariants = { initial: { opacity: 0, y: 12, filter: 'blur(4px)' }, animate: { opacity: 1, y: 0, filter: 'blur(0px)', transition: springs.drift }, exit: { opacity: 0, y: -8, filter: 'blur(4px)', transition: { duration: 0.2 } }, }; // Wrap in <AnimatePresence mode="wait"> at router level
jsxconst container = { hidden: {}, show: { transition: { staggerChildren: 0.07, delayChildren: 0.1 } }, }; const item = { hidden: { opacity: 0, y: 16 }, show: { opacity: 1, y: 0, transition: springs.default }, }; // Parent + children MUST be in same Client Component tree
jsx// useMotionValue OUTSIDE render — never useState const x = useMotionValue(0); const y = useMotionValue(0); const springX = useSpring(x, { stiffness: 200, damping: 20 }); const springY = useSpring(y, { stiffness: 200, damping: 20 }); function handleMouse(e) { const rect = ref.current.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; x.set((e.clientX - centerX) * 0.35); // 35% pull intensity y.set((e.clientY - centerY) * 0.35); } function handleLeave() { x.set(0); y.set(0); }
jsx// Source list item <motion.div layoutId={`card-${id}`}> <motion.h3 layoutId={`title-${id}`}>{title}</motion.h3> </motion.div> // Target detail view (same layoutId) <motion.div layoutId={`card-${id}`}> <motion.h1 layoutId={`title-${id}`}>{title}</motion.h1> </motion.div> // Wrap both in <LayoutGroup> at parent level
jsxconst { scrollYProgress } = useScroll({ target: ref, offset: ['start end','end start'] }); const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1]); const y = useTransform(scrollYProgress, [0, 0.3], [32, 0]);
CRITICAL: Never mix GSAP with Framer Motion in the same component. GSAP is for: scrolltelling, full-page sequences, canvas backgrounds.
jsxuseEffect(() => { const ctx = gsap.context(() => { // All GSAP code inside ctx gsap.from('.hero-title', { y: 60, opacity: 0, duration: 1, ease: 'expo.out', stagger: 0.08, }); ScrollTrigger.create({ trigger: sectionRef.current, start: 'top 80%', onEnter: () => gsap.to(sectionRef.current, { opacity: 1, y: 0, duration: 0.8 }), }); }, containerRef); return () => ctx.revert(); // ALWAYS clean up }, []);
jsxgsap.to(trackRef.current, { x: () => -(trackRef.current.scrollWidth - window.innerWidth), ease: 'none', scrollTrigger: { trigger: containerRef.current, pin: true, scrub: 1, end: () => `+=${trackRef.current.scrollWidth - window.innerWidth}`, }, });
Use only for: hero canvas backgrounds, 3D product showcases, data globe visualizations.
jsxuseEffect(() => { const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.current, alpha: true, antialias: true }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100); camera.position.z = 5; // ... scene setup ... let rafId; const animate = () => { rafId = requestAnimationFrame(animate); renderer.render(scene, camera); }; animate(); return () => { cancelAnimationFrame(rafId); renderer.dispose(); // Critical — prevents GPU memory leak scene.clear(); }; }, []);
BufferGeometry, never legacy Geometry.BufferGeometryUtils.mergeBufferGeometries.Premium CSS animations for when JS animation is overkill:
css/* Shimmer skeleton */ @keyframes shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } } .skeleton { background: linear-gradient(90deg, hsl(220 13% 91%) 25%, hsl(220 13% 96%) 50%, hsl(220 13% 91%) 75%); background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; } /* Status ping beacon */ @keyframes ping { 0% { transform: scale(1); opacity: 1; } 75% { transform: scale(2.2); opacity: 0; } 100% { transform: scale(2.2); opacity: 0; } } /* Directional hover fill (clip-path) */ .btn { position: relative; overflow: hidden; } .btn::before { content: ''; position: absolute; inset: 0; background: var(--accent-dim); clip-path: inset(0 100% 0 0); transition: clip-path 300ms cubic-bezier(0.16, 1, 0.3, 1); } .btn:hover::before { clip-path: inset(0 0% 0 0); } /* Float ambient animation */ @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
transform and opacity animated (no layout-triggering properties)will-change: transform removed after animation completesfixed pseudo-element only, not scrolling containersuseEffect cleanupReact.memo componentsprefers-reduced-motion respected with CSS media query + Framer useReducedMotion()staggerChildren parent + children in same Client ComponentOther measured skills in the registry, with their headline benchmark lift.