Awwwards Animations
Create premium web animations at Awwwards/FWA quality level. React-first approach. 60fps non-negotiable.
Decision Matrix
| Task | Library | Why |
|---|---|---|
| Scroll-driven animations | GSAP + ScrollTrigger + useGSAP | Industry standard, best control |
| Smooth scroll | Lenis + ReactLenis | Best performance, works with ScrollTrigger |
| React-native animations | Motion (Framer Motion) | Native React, useScroll/useTransform |
| Simple/lightweight effects | Anime.js 4.0 | Small footprint, clean API |
| Complex timelines | GSAP | Unmatched timeline control |
| SVG morphing | GSAP MorphSVG or Anime.js | Both excellent |
| 3D + animation | Three.js + GSAP | GSAP controls Three.js objects |
| Page transitions | AnimatePresence or GSAP | Motion for React, GSAP for complex |
| Geometric shapes (vector) | SVG + GSAP/Motion | Native, animable |
| Geometric shapes (canvas) | Canvas 2D API | Programmatic, performant |
| Pseudo-3D shapes | Zdog | Flat design 3D, ~2kb |
| Creative coding/generative | p5.js | Rich ecosystem |
| Audio reactive | Tone.js | Web Audio, synths, effects |
| Physics 2D | Matter.js | Gravity, collisions, constraints |
| Algorithmic/generative art | Canvas 2D + p5.js | Math-driven visuals |
| Fractals/L-systems | Canvas 2D recursivo | Recursive rendering |
| Tessellations/geometric puzzles | SVG + GSAP | Precise animated transforms |
| Kinetic typography advanced | GSAP SplitText + Canvas | Per-char control |
| Glitch effects | CSS + GSAP | Layered RGB split, clip-path |
| Brutalist animation | CSS raw + Motion | Hard cuts, no easing |
| Minimalist animation | Motion springs | Subtle, purposeful motion |
Installation (Latest Stable - 2025)
bash# GSAP + React hook (v3.14.1) npm install gsap @gsap/react # Lenis (v1.3.17) - includes React components npm install lenis # Motion (Framer Motion) npm install motion # Anime.js (v4.0.0) npm install animejs
React Setup
1. GSAP Configuration (app-wide)
tsx// lib/gsap.ts 'use client' // Next.js App Router import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { useGSAP } from '@gsap/react' // Register plugins once gsap.registerPlugin(ScrollTrigger, useGSAP) export { gsap, ScrollTrigger, useGSAP }
2. Lenis + GSAP ScrollTrigger Integration (Critical)
tsx// components/SmoothScroll.tsx 'use client' import { ReactLenis, useLenis } from 'lenis/react' import { useEffect } from 'react' import { gsap, ScrollTrigger } from '@/lib/gsap' export function SmoothScroll({ children }: { children: React.ReactNode }) { const lenis = useLenis() useEffect(() => { if (!lenis) return lenis.on('scroll', ScrollTrigger.update) gsap.ticker.add((time) => lenis.raf(time * 1000)) gsap.ticker.lagSmoothing(0) return () => { gsap.ticker.remove(lenis?.raf) } }, [lenis]) return ( <ReactLenis root options={{ lerp: 0.1, duration: 1.2, smoothWheel: true }}> {children} </ReactLenis> ) } // Wrap in layout: <SmoothScroll>{children}</SmoothScroll>
Core Patterns (React)
Detailed implementations in references:
- GSAP + useGSAP: See references/gsap-react.md
- Motion (Framer Motion): See references/motion-patterns.md
- Anime.js 4.0: See references/animejs-react.md
- Lenis React: See references/lenis-react.md
- Geometric Shapes: See references/geometric-shapes.md (SVG, Canvas, Zdog, p5.js, Tetris-style)
- Audio Reactive: See references/audio-reactive.md (Tone.js, Web Audio, scroll audio)
- Physics 2D: See references/physics-2d.md (Matter.js, collisions, constraints)
- Advanced (Three.js, WebGL): See references/advanced-patterns.md
- Algorithmic & Generative Art: See references/algorithmic-art.md (fractals, L-systems, flow fields, attractors, noise, sacred geometry)
- Advanced Text Effects: See references/text-effects.md (glitch, kinetic typography, morphing, explosion, circular text, scramble)
- Geometric Puzzles: See references/geometric-puzzles.md (Dudeney, tangram, tessellations, Penrose, polyominoes)
- Design Philosophy: See references/design-philosophy.md (brutalist, minimalist, abstract, mixing styles, palettes)
- Performance: See references/performance.md
Quick Patterns (React)
1. Magnetic Cursor (GSAP + useGSAP)
tsx'use client' import { useRef, useEffect } from 'react' import { gsap, useGSAP } from '@/lib/gsap' export function MagneticCursor() { const cursorRef = useRef<HTMLDivElement>(null) const pos = useRef({ x: 0, y: 0, cx: 0, cy: 0 }) useEffect(() => { const h = (e: MouseEvent) => { pos.current.x = e.clientX; pos.current.y = e.clientY } window.addEventListener('mousemove', h) return () => window.removeEventListener('mousemove', h) }, []) useGSAP(() => { gsap.ticker.add(() => { const p = pos.current p.cx += (p.x - p.cx) * 0.15; p.cy += (p.y - p.cy) * 0.15 gsap.set(cursorRef.current, { x: p.cx, y: p.cy }) }) }) return <div ref={cursorRef} className="fixed w-10 h-10 border border-white rounded-full pointer-events-none mix-blend-difference z-[9999] -translate-x-1/2 -translate-y-1/2" /> }
2. Magnetic Button (Motion)
tsx'use client' import { useRef, useState } from 'react' import { motion } from 'motion/react' export function MagneticButton({ children }: { children: React.ReactNode }) { const ref = useRef<HTMLButtonElement>(null) const [pos, setPos] = useState({ x: 0, y: 0 }) const onMove = (e: React.MouseEvent) => { const { left, top, width, height } = ref.current!.getBoundingClientRect() setPos({ x: (e.clientX - left - width / 2) * 0.3, y: (e.clientY - top - height / 2) * 0.3 }) } return ( <motion.button ref={ref} onMouseMove={onMove} onMouseLeave={() => setPos({ x: 0, y: 0 })} animate={pos} transition={{ type: 'spring', stiffness: 150, damping: 15 }} className="px-8 py-4 bg-white text-black rounded-full">{children}</motion.button> ) }
3. Parallax Hero (GSAP + useGSAP)
tsx'use client' import { useRef } from 'react' import { gsap, ScrollTrigger, useGSAP } from '@/lib/gsap' export function ParallaxHero() { const containerRef = useRef<HTMLDivElement>(null) useGSAP(() => { gsap.to('.parallax-bg', { yPercent: 50, ease: 'none', scrollTrigger: { trigger: containerRef.current, start: 'top top', end: 'bottom top', scrub: true, }, }) gsap.to('.hero-title', { yPercent: 100, opacity: 0, scrollTrigger: { trigger: containerRef.current, start: 'top top', end: '50% top', scrub: true, }, }) }, { scope: containerRef }) return ( <div ref={containerRef} className="relative h-screen overflow-hidden"> <div className="parallax-bg absolute inset-0 bg-cover bg-center" /> <h1 className="hero-title absolute inset-0 flex items-center justify-center text-6xl"> Hero Title </h1> </div> ) }
4. Text Character Reveal (Motion)
tsx'use client' import { motion } from 'motion/react' const container = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.02 }, }, } const child = { hidden: { opacity: 0, y: 50, rotateX: -90 }, visible: { opacity: 1, y: 0, rotateX: 0, transition: { type: 'spring', damping: 12 }, }, } export function TextReveal({ text }: { text: string }) { return ( <motion.span variants={container} initial="hidden" whileInView="visible" viewport={{ once: true }} className="inline-block" > {text.split('').map((char, i) => ( <motion.span key={i} variants={child} className="inline-block"> {char === ' ' ? '\u00A0' : char} </motion.span> ))} </motion.span> ) }
5. Image Reveal (GSAP)
tsx'use client' import { useRef } from 'react' import { gsap, useGSAP } from '@/lib/gsap' export function ImageReveal({ src, alt }: { src: string; alt: string }) { const containerRef = useRef<HTMLDivElement>(null) useGSAP(() => { gsap.from(containerRef.current, { clipPath: 'inset(100% 0% 0% 0%)', duration: 1.2, ease: 'power4.inOut', scrollTrigger: { trigger: containerRef.current, start: 'top 80%', }, }) gsap.from('.reveal-img', { scale: 1.3, duration: 1.5, ease: 'power2.out', scrollTrigger: { trigger: containerRef.current, start: 'top 80%', }, }) }, { scope: containerRef }) return ( <div ref={containerRef} className="overflow-hidden"> <img src={src} alt={alt} className="reveal-img w-full h-full object-cover" /> </div> ) }
6. Glitch Text Effect (CSS + GSAP)
tsx'use client' import { useRef, useEffect } from 'react' import { gsap } from '@/lib/gsap' export function GlitchText({ text }: { text: string }) { const ref = useRef<HTMLDivElement>(null) useEffect(() => { const layers = ref.current!.querySelectorAll('.g-layer') const tl = gsap.timeline({ repeat: -1, repeatDelay: 3 }) tl.to(layers[0], { x: -5, duration: 0.05, ease: 'none' }, 0) .to(layers[0], { x: 5, duration: 0.05 }, 0.05) .to(layers[0], { x: 0, duration: 0.05 }, 0.1) .to(layers[1], { x: 5, duration: 0.05 }, 0.02) .to(layers[1], { x: -5, duration: 0.05 }, 0.07) .to(layers[1], { x: 0, duration: 0.05 }, 0.12) return () => { tl.kill() } }, []) return ( <div ref={ref} className="relative font-mono text-5xl font-black"> <span className="relative z-10">{text}</span> <span className="g-layer absolute inset-0 text-cyan-400 mix-blend-multiply" aria-hidden>{text}</span> <span className="g-layer absolute inset-0 text-red-400 mix-blend-multiply" aria-hidden>{text}</span> </div> ) }
7. Fractal Tree (Canvas 2D)
tsx'use client' import { useRef, useEffect } from 'react' export function FractalTree({ depth = 10, angle = 25 }: { depth?: number; angle?: number }) { const canvasRef = useRef<HTMLCanvasElement>(null) useEffect(() => { const canvas = canvasRef.current! const ctx = canvas.getContext('2d')! canvas.width = canvas.offsetWidth * 2; canvas.height = canvas.offsetHeight * 2; ctx.scale(2, 2) let progress = 0, raf = 0 function branch(x: number, y: number, len: number, a: number, d: number) { if (d > depth || len < 2) return const dp = Math.max(0, Math.min(1, progress * depth - d)) if (dp <= 0) return const ex = x + Math.cos(a * Math.PI / 180) * len * dp const ey = y - Math.sin(a * Math.PI / 180) * len * dp ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(ex, ey) ctx.strokeStyle = `hsl(${120 + d * 15}, 60%, ${30 + d * 5}%)` ctx.lineWidth = Math.max(1, (depth - d) * 1.5); ctx.stroke() branch(ex, ey, len * 0.72, a + angle, d + 1) branch(ex, ey, len * 0.72, a - angle, d + 1) } const animate = () => { progress = Math.min(1, progress + 0.008) ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight) branch(canvas.offsetWidth / 2, canvas.offsetHeight, canvas.offsetHeight * 0.28, 90, 0) if (progress < 1) raf = requestAnimationFrame(animate) } animate() return () => cancelAnimationFrame(raf) }, [depth, angle]) return <canvas ref={canvasRef} className="w-full h-full bg-gray-950" /> }
See references/algorithmic-art.md for L-systems, flow fields, attractors, noise, sacred geometry.
8. Geometric Dissection (SVG + GSAP)
tsx'use client' import { useRef, useState } from 'react' import { gsap } from '@/lib/gsap' const P = [ { id: 'A', tri: 'M 0,173 L 50,87 L 100,173 Z', sq: 'M 0,0 L 100,0 L 100,87 L 0,87 Z', c: '#f43f5e' }, { id: 'B', tri: 'M 50,87 L 100,0 L 150,87 Z', sq: 'M 100,0 L 200,0 L 200,87 L 100,87 Z', c: '#8b5cf6' }, { id: 'C', tri: 'M 100,173 L 150,87 L 200,173 Z', sq: 'M 0,87 L 100,87 L 100,173 L 0,173 Z', c: '#06b6d4' }, { id: 'D', tri: 'M 50,87 L 100,173 L 150,87 L 100,0 Z', sq: 'M 100,87 L 200,87 L 200,173 L 100,173 Z', c: '#f59e0b' }, ] export function GeometricDissection() { const svg = useRef<SVGSVGElement>(null) const [isSq, setSq] = useState(false) const morph = () => { const t = !isSq P.forEach((p, i) => { const el = svg.current!.querySelector(`#d-${p.id}`) if (el) gsap.to(el, { attr: { d: t ? p.sq : p.tri }, duration: 1.5, ease: 'power2.inOut', delay: i * 0.15 }) }); setSq(t) } return ( <div className="flex flex-col items-center gap-4"> <svg ref={svg} viewBox="-10 -10 220 200" className="w-64 h-64"> {P.map(p => <path key={p.id} id={`d-${p.id}`} d={p.tri} fill={p.c} stroke="#000" strokeWidth="1.5" />)} </svg> <button onClick={morph} className="px-6 py-2 bg-white text-black font-mono text-sm">{isSq ? '△' : '□'}</button> </div> ) }
See references/geometric-puzzles.md for tangram, tessellations, Penrose tiles, polyominoes.
9. Brutalist Grid (Motion)
tsx'use client' import { motion } from 'motion/react' export function BrutalistGrid({ items }: { items: string[] }) { return ( <div className="grid grid-cols-3 border-2 border-black"> {items.map((item, i) => ( <motion.div key={i} className="border-2 border-black p-6 font-mono font-black uppercase text-2xl" style={{ mixBlendMode: i % 2 === 0 ? 'normal' : 'difference' }} initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ duration: 0, delay: i * 0.1 }} whileHover={{ backgroundColor: '#000', color: '#BAFF39', transition: { duration: 0 } }} >{item}</motion.div> ))} </div> ) }
Design Philosophy (Quick Reference)
| Style | Motion Feel | Easing | Typography | Key Trait |
|---|---|---|---|---|
| Brutalist | Hard, instant, jarring | none / steps() | Mono, 15-30vw | Raw honesty |
| Minimalist | Smooth, subtle, slow | power2.out | Sans-serif light | Purposeful restraint |
| Abstract | Noise-driven, parametric | Organic/sine | Varies | Mathematical beauty |
| Neo-Brutalist | Bold but controlled | power1.out | Mono + color | Brutalism + restraint |
See references/design-philosophy.md for full guide with color palettes and mixing strategies.
Easing Reference
| Feel | GSAP | Motion |
|---|---|---|
| Smooth | power2.out | [0.16, 1, 0.3, 1] |
| Snappy | power4.out | [0.87, 0, 0.13, 1] |
| Bouncy | back.out(1.7) | { type: 'spring', stiffness: 300, damping: 20 } |
| Dramatic | power4.inOut | [0.76, 0, 0.24, 1] |
Timing
- Micro-interactions: 150-300ms
- UI transitions: 300-500ms
- Page transitions: 500-800ms
- Stagger: 0.02-0.1s per item
Accessibility
tsx// Motion: useReducedMotion() → conditionally disable/reduce animations import { useReducedMotion } from 'motion/react' const reduced = useReducedMotion() // true if prefers-reduced-motion: reduce
css@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
Performance Rules
- Only animate
transformandopacity - Use
will-changesparingly - Always cleanup:
useGSAPhandles it automatically - Scope GSAP selectors to container refs
- Use
contextSafe()for event handlers with GSAP - Memoize Motion variants objects
Common Pitfalls
- Not integrating Lenis with ScrollTrigger
- Missing
scopein useGSAP - Not using
contextSafe()for click handlers - React 18 Strict Mode calling effects twice
- Forgetting
'use client'in Next.js App Router - Not calling
ScrollTrigger.refresh()after dynamic content
Testing Checklist
- 60fps on scroll (Chrome DevTools Performance)
- Keyboard navigation works
- Respects prefers-reduced-motion
- No layout shifts (CLS)
- Mobile touch works
- ScrollTrigger markers removed in prod
- No memory leaks on unmount
Inspiration
Active Theory, Studio Freight, Locomotive, Resn, Aristide Benoist, Immersive Garden


