14 KiB
14 KiB
Anime.js 4.0 React Patterns
Complete Anime.js 4.0 patterns for React. Lightweight alternative for simple animations.
Table of Contents
- Installation & Setup
- React Integration
- Basic Animations
- Timeline
- Stagger
- Scroll Animations
- Text Animations
- SVG Animations
- Draggable
- v3 to v4 Migration
Installation & Setup
npm install animejs
Imports (v4 Syntax)
// Named imports (v4)
import {
animate,
createTimeline,
createScope,
createSpring,
createDraggable,
stagger,
svg,
utils,
engine
} from 'animejs'
React Integration
Basic Pattern with createScope
'use client'
import { useEffect, useRef } from 'react'
import { animate, createScope } from 'animejs'
export function AnimatedBox() {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
// Create scope bound to container
scopeRef.current = createScope({ root: rootRef.current }).add(() => {
// All animations here are scoped to rootRef
animate('.box', {
translateX: 250,
rotate: '1turn',
duration: 800,
ease: 'out(3)',
})
})
// Cleanup on unmount
return () => scopeRef.current?.revert()
}, [])
return (
<div ref={rootRef}>
<div className="box">Animated</div>
</div>
)
}
Registering Methods for External Control
'use client'
import { useEffect, useRef } from 'react'
import { animate, createScope } from 'animejs'
export function ControlledAnimation() {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: rootRef.current }).add((self) => {
// Register method accessible outside useEffect
self.add('animateBox', () => {
animate('.box', {
scale: [1, 1.2, 1],
duration: 400,
ease: 'out(2)',
})
})
})
return () => scopeRef.current?.revert()
}, [])
const handleClick = () => {
// Call registered method
scopeRef.current?.methods.animateBox()
}
return (
<div ref={rootRef}>
<button onClick={handleClick}>Animate</button>
<div className="box">Click to animate</div>
</div>
)
}
Basic Animations
Simple Animation
// v4 syntax: animate(targets, { properties })
animate('.element', {
translateX: 250,
translateY: 100,
rotate: '1turn',
scale: 1.5,
opacity: 0.5,
duration: 1000,
ease: 'out(3)',
})
From/To Values
animate('.element', {
translateX: [0, 250], // from 0 to 250
opacity: [0, 1], // from 0 to 1
scale: [0.5, 1], // from 0.5 to 1
duration: 800,
})
Keyframes
animate('.element', {
keyframes: [
{ translateX: 0, scale: 1 },
{ translateX: 100, scale: 1.2 },
{ translateX: 200, scale: 1 },
{ translateX: 250, scale: 0.8 },
],
duration: 2000,
ease: 'inOut(2)',
})
Property-Specific Parameters
animate('.element', {
translateX: {
to: 250,
duration: 1000,
ease: 'out(4)',
},
rotate: {
to: '1turn',
duration: 1500,
ease: 'inOut(2)',
},
scale: {
to: 1.5,
duration: 800,
delay: 200,
},
})
Callbacks
animate('.element', {
translateX: 250,
duration: 1000,
onBegin: () => console.log('Started'),
onUpdate: (anim) => console.log(anim.progress),
onComplete: () => console.log('Done'),
onLoop: () => console.log('Loop'),
}).then(() => {
console.log('Promise resolved')
})
Playback Controls
const animation = animate('.element', {
translateX: 250,
autoplay: false,
})
animation.play() // Play forward
animation.pause() // Pause
animation.resume() // Resume in current direction
animation.reverse() // Play backward
animation.restart() // Restart from beginning
animation.seek(500) // Seek to 500ms
animation.reset() // Reset to initial state
Timeline
Basic Timeline
import { createTimeline } from 'animejs'
const tl = createTimeline({
defaults: {
duration: 500,
ease: 'out(3)',
}
})
tl.add('.box-1', { translateX: 250 })
.add('.box-2', { translateX: 250 }, '-=200') // 200ms before previous ends
.add('.box-3', { translateX: 250 }, '+=100') // 100ms after previous ends
Timeline in React
'use client'
import { useEffect, useRef } from 'react'
import { createTimeline, createScope } from 'animejs'
export function TimelineAnimation() {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: rootRef.current }).add(() => {
const tl = createTimeline({
defaults: { duration: 600, ease: 'out(3)' }
})
tl.add('.title', { opacity: [0, 1], translateY: [30, 0] })
.add('.subtitle', { opacity: [0, 1], translateY: [20, 0] }, '-=400')
.add('.cta', { opacity: [0, 1], scale: [0.9, 1] }, '-=300')
})
return () => scopeRef.current?.revert()
}, [])
return (
<div ref={rootRef}>
<h1 className="title">Title</h1>
<p className="subtitle">Subtitle</p>
<button className="cta">CTA</button>
</div>
)
}
Stagger
Basic Stagger
import { stagger } from 'animejs'
animate('.grid-item', {
opacity: [0, 1],
translateY: [50, 0],
delay: stagger(100), // 100ms between each
duration: 600,
})
Stagger with Start Value
animate('.item', {
translateX: 250,
delay: stagger(100, { start: 500 }), // Start at 500ms, then +100ms each
})
Grid Stagger
animate('.grid-item', {
scale: [0, 1],
delay: stagger(50, {
grid: [4, 4], // 4x4 grid
from: 'center', // Animate from center outward
}),
})
Stagger from Index
animate('.item', {
opacity: [0, 1],
delay: stagger(100, {
from: 'first', // 'first', 'last', 'center', or index number
}),
})
Value Stagger
animate('.bar', {
scaleY: stagger([0.5, 1]), // Scale from 0.5 to 1 distributed
duration: 800,
})
Scroll Animations
ScrollObserver
'use client'
import { useEffect, useRef } from 'react'
import { animate, createScope } from 'animejs'
export function ScrollReveal() {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: rootRef.current }).add(() => {
// Using Intersection Observer pattern
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animate(entry.target, {
opacity: [0, 1],
translateY: [50, 0],
duration: 800,
ease: 'out(3)',
})
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.2 }
)
document.querySelectorAll('.reveal-item').forEach((el) => {
observer.observe(el)
})
})
return () => scopeRef.current?.revert()
}, [])
return (
<div ref={rootRef}>
{[1, 2, 3, 4].map((i) => (
<div key={i} className="reveal-item opacity-0">
Item {i}
</div>
))}
</div>
)
}
Text Animations
Character Split Animation
'use client'
import { useEffect, useRef } from 'react'
import { animate, createScope, stagger } from 'animejs'
export function TextReveal({ text }: { text: string }) {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: rootRef.current }).add(() => {
animate('.char', {
opacity: [0, 1],
translateY: [50, 0],
rotateX: [-90, 0],
delay: stagger(30),
duration: 600,
ease: 'out(3)',
})
})
return () => scopeRef.current?.revert()
}, [])
return (
<div ref={rootRef} className="overflow-hidden">
{text.split('').map((char, i) => (
<span key={i} className="char inline-block opacity-0">
{char === ' ' ? '\u00A0' : char}
</span>
))}
</div>
)
}
Word Animation
export function WordReveal({ text }: { text: string }) {
const rootRef = useRef<HTMLDivElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: rootRef.current }).add(() => {
animate('.word', {
opacity: [0, 1],
translateY: ['100%', '0%'],
delay: stagger(80),
duration: 800,
ease: 'out(4)',
})
})
return () => scopeRef.current?.revert()
}, [])
return (
<div ref={rootRef}>
{text.split(' ').map((word, i) => (
<span key={i} className="inline-block overflow-hidden mr-2">
<span className="word inline-block opacity-0">{word}</span>
</span>
))}
</div>
)
}
SVG Animations
Path Drawing
import { svg } from 'animejs'
export function DrawSVG() {
const svgRef = useRef<SVGSVGElement>(null)
const scopeRef = useRef<ReturnType<typeof createScope> | null>(null)
useEffect(() => {
scopeRef.current = createScope({ root: svgRef.current }).add(() => {
const drawable = svg.createDrawable('.draw-path')
animate(drawable, {
draw: ['0 0', '0 1'], // From 0% to 100%
duration: 2000,
ease: 'inOut(2)',
})
})
return () => scopeRef.current?.revert()
}, [])
return (
<svg ref={svgRef} viewBox="0 0 100 100">
<path
className="draw-path"
d="M10,50 Q50,10 90,50 T90,90"
fill="none"
stroke="white"
strokeWidth="2"
/>
</svg>
)
}
SVG Morphing
import { svg } from 'animejs'
export function MorphSVG() {
const pathRef = useRef<SVGPathElement>(null)
useEffect(() => {
const morph = svg.createMorph(pathRef.current)
animate(morph, {
to: 'M50,10 A40,40 0 1,1 50,90 A40,40 0 1,1 50,10', // Circle
duration: 1000,
ease: 'inOut(2)',
})
}, [])
return (
<svg viewBox="0 0 100 100">
<path ref={pathRef} d="M50,10 L90,90 L10,90 Z" fill="white" />
</svg>
)
}
Motion Path
import { svg } from 'animejs'
export function MotionPath() {
const elementRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const motionPath = svg.createMotionPath('#motion-path')
animate(elementRef.current, {
translateX: motionPath.x,
translateY: motionPath.y,
rotate: motionPath.angle,
duration: 3000,
ease: 'linear',
loop: true,
})
}, [])
return (
<>
<svg className="absolute">
<path id="motion-path" d="M0,100 Q250,0 500,100" fill="none" />
</svg>
<div ref={elementRef} className="w-4 h-4 bg-white rounded-full" />
</>
)
}
Draggable
Basic Draggable
import { createDraggable } from 'animejs'
export function DraggableBox() {
const boxRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const draggable = createDraggable(boxRef.current, {
trigger: boxRef.current,
releaseEase: 'out(3)',
releaseStiffness: 50,
})
return () => draggable.revert()
}, [])
return (
<div ref={boxRef} className="w-20 h-20 bg-white cursor-grab">
Drag me
</div>
)
}
Draggable with Constraints
createDraggable(element, {
container: containerRef.current, // Constrain to container
x: { min: 0, max: 500 }, // X bounds
y: { min: 0, max: 300 }, // Y bounds
snap: { x: 50, y: 50 }, // Snap to grid
})
Easing Reference (v4)
Built-in Easings
// v4 easing syntax (no 'ease' prefix)
ease: 'linear'
ease: 'in(2)' // Power in
ease: 'out(2)' // Power out (default)
ease: 'inOut(2)' // Power in-out
ease: 'out(4)' // Stronger ease out
Spring Easing
import { createSpring } from 'animejs'
const spring = createSpring({
mass: 1,
stiffness: 100,
damping: 10,
velocity: 0,
})
animate('.element', {
translateX: 250,
ease: spring,
})
Custom Easing
// Cubic bezier
ease: 'cubicBezier(0.76, 0, 0.24, 1)'
// Custom function
ease: (t) => t * t // Quadratic
v3 to v4 Migration
| v3 | v4 |
|---|---|
anime({ targets, ...props }) |
animate(targets, { ...props }) |
easing: 'easeOutQuad' |
ease: 'out(2)' |
easing: 'easeInOutCubic' |
ease: 'inOut(3)' |
endDelay |
loopDelay |
direction: 'reverse' |
reversed: true |
direction: 'alternate' |
alternate: true |
update callback |
onUpdate callback |
begin callback |
onBegin callback |
complete callback |
onComplete callback |
.finished.then() |
.then() |
anime.timeline() |
createTimeline() |
anime.stagger() |
stagger() |
When to Use Anime.js vs GSAP
| Use Anime.js | Use GSAP |
|---|---|
| Simple animations | Complex scroll-driven |
| Lightweight needs (~17kb) | ScrollTrigger required |
| Quick prototypes | Production timelines |
| SVG morphing | SplitText, MorphSVG |
| Draggable elements | Pin sections |