/SKILL.md
This skill should be used when the user asks to "animate elements", "add animations", "create transitions", "use anime.js", "implement motion", "add scroll animations", "create timeline animations", "stagger animations", "animate SVG", "add draggable elements", or needs guidance on anime.js v4 patterns, easing, performance, or React/Next.js animation integration.
npx skillsauth add santiagoasda/animejs-best-practices anime.js Best PracticesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A comprehensive guide for implementing animations with anime.js v4 - the lightweight JavaScript animation engine.
anime.js v4 provides a powerful API for animating CSS properties, SVG attributes, DOM elements, and JavaScript objects. This skill covers best practices, common patterns, and performance optimization for both vanilla JavaScript and React/Next.js applications.
Always prefer waapi.animate() over animate() unless you need JS engine features.
| Use waapi.animate() when: | Use animate() when: |
|----------------------------|----------------------|
| ✅ Simple CSS transforms/opacity | ❌ Animating 500+ targets |
| ✅ Bundle size matters (3KB vs 10KB) | ❌ Animating JS objects, Canvas, WebGL |
| ✅ CPU/network load is high | ❌ Complex timeline orchestration |
| ✅ Need hardware acceleration | ❌ Need extensive callbacks (onRender, etc.) |
| ✅ CSS color functions | ❌ SVG path morphing |
// Recommended: from main module
import { waapi, stagger, splitText } from 'animejs';
// Or standalone (smallest bundle)
import { waapi } from 'animejs/waapi';
// WAAPI - hardware accelerated, lightweight
waapi.animate('.element', {
translateX: 200,
rotate: 180,
opacity: [0, 1],
duration: 800,
ease: 'outExpo'
});
| Property | CSS | WAAPI/anime.js |
|----------|-----|----------------|
| Duration | 2s | 2000 (milliseconds!) |
| Infinite | infinite | Infinity (JS constant) |
| Default easing | ease | linear |
| Iterations | animation-iteration-count | iterations |
Common bug: Animation not visible because duration is 2 (2ms) instead of 2000 (2s).
import { animate } from 'animejs';
// Use animate() for: 500+ targets, JS objects, complex timelines, SVG morphing
animate(myJsObject, {
value: 100,
onUpdate: () => renderCanvas(myJsObject) // Extensive callbacks
});
anime.js waapi.animate() wraps the browser's native Element.animate(). Use native when:
commitStyles(), persist(), getAnimations()// Native WAAPI (no anime.js needed)
element.animate([
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }
], {
duration: 500, // ALWAYS milliseconds
easing: 'ease-out', // default is 'linear', not 'ease'!
fill: 'forwards'
});
See references/native-waapi.md for complete native WAAPI documentation.
Before writing any animation code, ALWAYS follow this planning workflow:
Present an ASCII/text visualization of the animation to the user:
Animation: "Bouncing Text"
Target: "hello world" (11 characters)
Timeline visualization:
t=0ms h e l l o w o r l d [all at baseline]
t=100ms H e l l o w o r l d [h bounces up]
t=180ms h E l l o w o r l d [e bounces up]
t=260ms h e L l o w o r l d [l bounces up]
...continues with 80ms stagger...
Movement pattern per character:
↑ (-20px)
│
────┼──── baseline
│
↓ (return)
Duration: 300ms up + 300ms down = 600ms per bounce
Loop: continuous
Total animation time: 3000ms requested
Decide which animation engine to use:
Engine Decision:
- Target count: 11 characters (< 500) ✅ WAAPI OK
- Animation type: CSS transforms (translateY) ✅ WAAPI OK
- Callbacks needed: None ✅ WAAPI OK
- Bundle size: Want smallest ✅ WAAPI preferred
→ Decision: Use waapi.animate()
List the specific anime.js functions required:
APIs needed:
- splitText() - to split "hello world" into individual character elements
- waapi.animate() - hardware-accelerated animation (WAAPI-first!)
- stagger() - to offset each character's animation start time
Why splitText() over manual splitting:
- Handles accessibility (aria-label)
- Provides proper data attributes (data-char, data-word, data-line)
- Handles whitespace correctly
- Returns animatable element arrays directly
Show the math for timing. Remember: WAAPI uses milliseconds, not seconds!
Timing calculation (all values in MILLISECONDS):
- User requested: 3 seconds = 3000ms
- Bounce cycle: 300ms up + 300ms down = 600ms
- Characters: 11
- Stagger delay: 80ms between each
- Last char starts at: 10 × 80ms = 800ms
- Full sequence: 800ms + 600ms = 1400ms
- For 3000ms total: loop: true (repeats ~2x)
⚠️ VERIFY: duration: 600 (not 0.6!)
⚠️ VERIFY: delay: 80 (not 0.08!)
Verify accessibility is handled:
Accessibility:
- [ ] Will respect prefers-reduced-motion? YES - will add check
- [ ] Duration reasonable? YES - 600ms (not too fast/slow)
- [ ] Essential content visible without animation? YES
Always ask: "Does this animation plan look correct? Should I proceed with implementation?"
Only after confirmation, write the code using the exact APIs specified.
npm install animejs
Use named imports from the main module (recommended for bundlers with tree-shaking):
import { animate, createTimeline, stagger, utils } from 'animejs';
For projects without bundlers or when tree-shaking is ineffective, use subpath imports:
import { animate } from 'animejs/animation';
import { createTimeline } from 'animejs/timeline';
import { stagger, random } from 'animejs/utils';
import { splitText } from 'animejs/text';
import { svg } from 'animejs/svg';
import { waapi } from 'animejs';
// Use WAAPI for CSS transforms - hardware accelerated!
waapi.animate('.element', {
translateX: 250,
rotate: '1turn',
opacity: 0.5,
duration: 1000,
ease: 'outExpo'
});
Both waapi.animate() and animate() accept multiple target formats:
import { waapi } from 'animejs';
// CSS selector
waapi.animate('.my-class', { opacity: 0 });
// DOM element
waapi.animate(document.querySelector('#element'), { scale: 1.5 });
// Array of elements
waapi.animate([el1, el2, el3], { translateY: 100 });
import { animate } from 'animejs';
// Use animate() for JavaScript objects (WAAPI can't do this)
const obj = { value: 0 };
animate(obj, {
value: 100,
duration: 1000,
onUpdate: () => console.log(obj.value)
});
translateX, translateY, translateZ, rotate, rotateX, rotateY, rotateZ, scale, scaleX, scaleY, skew, skewX, skewYopacity, width, height, margin, padding, borderRadius, backgroundColor, color'--custom-property'cx, cy, r, fill, stroke, strokeDashoffset, points, danimate('.element', {
// Absolute values
translateX: 250,
// With units
width: '100%',
// Relative values
translateY: '+=50', // Add 50 to current
rotate: '-=45deg', // Subtract 45deg
// From-to array
opacity: [0, 1], // Animate from 0 to 1
// Function-based (per target)
scale: (el, i) => 1 + i * 0.1,
// Keyframes
translateX: [
{ value: 100, duration: 500 },
{ value: 0, duration: 500 }
]
});
Note: Complex timelines require the JS engine (createTimeline), not WAAPI.
Orchestrate multiple animations with precise control:
import { createTimeline } from 'animejs';
const tl = createTimeline({
defaults: { duration: 800, ease: 'outExpo' }
});
tl.add('.box-1', { translateX: 250 })
.add('.box-2', { translateY: 100 }, '-=400') // Start 400ms before previous ends
.add('.box-3', { scale: 1.5 }, '+=200'); // Start 200ms after previous ends
const tl = createTimeline();
tl.add('.intro', { opacity: [0, 1] })
.label('afterIntro')
.add('.content', { translateY: [50, 0] }, 'afterIntro')
.add('.sidebar', { translateX: [-100, 0] }, 'afterIntro+=200');
const tl = createTimeline({ autoplay: false });
// Add animations...
tl.play();
tl.pause();
tl.reverse();
tl.seek(500); // Jump to 500ms
tl.restart();
tl.complete(); // Jump to end
Create sequential animations across multiple elements (works with both WAAPI and JS engine):
import { waapi, stagger } from 'animejs';
// Time staggering (WAAPI)
waapi.animate('.item', {
translateY: [50, 0],
opacity: [0, 1],
delay: stagger(100) // 100ms between each
});
// Grid staggering (WAAPI)
waapi.animate('.grid-item', {
scale: [0, 1],
delay: stagger(50, {
grid: [10, 10],
from: 'center',
axis: 'y'
})
});
// Stagger with easing (WAAPI)
waapi.animate('.item', {
translateX: 200,
delay: stagger(100, { ease: 'outQuad' })
});
import { animate, stagger } from 'animejs';
// Value staggering requires JS engine
animate('.bar', {
height: stagger([100, 200, 150, 250]) // Specific values per element
});
ALWAYS use splitText() for character/word/line animations - never manually split text.
import { waapi, splitText, stagger } from 'animejs';
// Split text into characters, words, and lines
const { chars, words, lines } = splitText('.text-element', {
chars: true,
words: true,
lines: true
});
// Animate characters with WAAPI (hardware accelerated)
waapi.animate(chars, {
translateY: [-20, 0],
opacity: [0, 1],
delay: stagger(50),
duration: 600,
ease: 'outExpo'
});
const split = splitText('.text', {
// What to split into
chars: true, // Split into characters
words: true, // Split into words
lines: true, // Split into lines
// Wrap elements for clipping effects
chars: { wrap: 'span', class: 'char' },
words: { wrap: 'clip' }, // 'clip' adds overflow:hidden for reveal effects
lines: { wrap: 'div' },
// Accessibility
accessible: true, // Adds aria-label to preserve screen reader text
// Include space characters as elements
includeSpaces: true
});
// Access results
split.chars // Array of character span elements
split.words // Array of word elements
split.lines // Array of line elements
import { waapi, splitText, stagger } from 'animejs';
// Split the text
const { chars } = splitText('.bouncing-text', { chars: true });
// Bouncing animation with WAAPI
waapi.animate(chars, {
translateY: [-20, 0], // Simple keyframes for WAAPI
delay: stagger(80),
duration: 600, // Full bounce cycle
loop: true,
alternate: true, // Bounce back down
ease: 'inOutQuad'
});
import { waapi, splitText, stagger } from 'animejs';
const { chars } = splitText('.wave-text', { chars: true });
waapi.animate(chars, {
translateY: -15,
delay: stagger(40, { from: 'center' }),
duration: 400,
loop: true,
alternate: true,
ease: 'inOutSine'
});
import { waapi, splitText, stagger } from 'animejs';
const { words } = splitText('.reveal-text', {
words: { wrap: 'clip' } // Clip overflow for reveal effect
});
waapi.animate(words, {
translateY: ['100%', '0%'],
delay: stagger(100),
duration: 800,
ease: 'outExpo'
});
'use client';
import { useEffect, useRef } from 'react';
import { waapi, splitText, stagger } from 'animejs';
export function AnimatedText({ children, className }) {
const textRef = useRef(null);
useEffect(() => {
if (!textRef.current) return;
const { chars } = splitText(textRef.current, {
chars: true,
accessible: true
});
// Use WAAPI for hardware-accelerated text animation
const anim = waapi.animate(chars, {
translateY: [20, 0],
opacity: [0, 1],
delay: stagger(30),
duration: 600,
ease: 'outExpo'
});
return () => anim.cancel();
}, [children]);
return <span ref={textRef} className={className}>{children}</span>;
}
Pattern: {in|out|inOut}{Quad|Cubic|Quart|Quint|Sine|Expo|Circ|Back|Elastic|Bounce}
animate('.element', {
translateX: 200,
ease: 'outExpo' // Common choice for UI
});
animate('.element', {
translateX: 200,
ease: 'inOutQuad' // Smooth acceleration/deceleration
});
import { cubicBezier } from 'animejs';
animate('.element', {
translateX: 200,
ease: cubicBezier(0.7, 0.1, 0.5, 0.9)
});
import { spring } from 'animejs';
animate('.element', {
translateX: 200,
ease: spring({ bounce: 0.25, duration: 800 })
});
// Stiffness/damping control
animate('.element', {
scale: 1.2,
ease: spring({ stiffness: 90, damping: 14 })
});
animate('.element', {
translateX: 200,
ease: 'steps(5)' // 5 discrete steps
});
animate('.element', {
translateX: 200,
onBegin: (anim) => console.log('Started'),
onUpdate: (anim) => console.log(anim.progress),
onComplete: (anim) => console.log('Done'),
onLoop: (anim) => console.log('Loop iteration')
});
await animate('.step-1', { opacity: [0, 1] }).then();
await animate('.step-2', { translateY: [50, 0] }).then();
await animate('.step-3', { scale: [0.8, 1] }).then();
const anim = animate('.element', {
translateX: 200,
autoplay: false
});
anim.play();
anim.pause();
anim.reverse();
anim.restart();
anim.seek(500); // Jump to 500ms
anim.stretch(2000); // Change duration to 2000ms
anim.playbackRate = 2; // Double speed
import { useRef, useEffect } from 'react';
import { waapi } from 'animejs';
function AnimatedComponent() {
const elementRef = useRef(null);
useEffect(() => {
if (elementRef.current) {
// WAAPI for hardware-accelerated animations
waapi.animate(elementRef.current, {
translateY: [20, 0],
opacity: [0, 1],
duration: 600,
ease: 'outExpo'
});
}
}, []);
return <div ref={elementRef}>Animated content</div>;
}
useEffect(() => {
const anim = waapi.animate(elementRef.current, {
translateX: [0, 200],
loop: true
});
return () => anim.cancel(); // Cancel on unmount
}, []);
import { useRef, useEffect, useCallback } from 'react';
import { waapi } from 'animejs';
function useAnimation(config) {
const elementRef = useRef(null);
const animationRef = useRef(null);
const play = useCallback(() => {
if (elementRef.current) {
animationRef.current = waapi.animate(elementRef.current, config);
}
}, [config]);
useEffect(() => {
return () => animationRef.current?.cancel();
}, []);
return { ref: elementRef, play };
}
Animation code must run client-side. Mark components with 'use client':
'use client';
import { useEffect, useRef } from 'react';
import { waapi } from 'animejs';
export function AnimatedCard({ children }) {
const ref = useRef(null);
useEffect(() => {
// WAAPI is ideal for simple entrance animations
waapi.animate(ref.current, {
opacity: [0, 1],
translateY: [20, 0],
duration: 600,
ease: 'outExpo'
});
}, []);
return <div ref={ref}>{children}</div>;
}
ALWAYS respect user preferences for reduced motion.
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
// No animation - instant state change
element.style.opacity = '1';
} else {
waapi.animate(element, { opacity: [0, 1], duration: 600 });
}
See references/accessibility-and-testing.md for React hook and full patterns.
translateX/Y, scale, rotate instead of width, height, top, leftwill-change sparingly: Add only when needed, remove after animationcancel() when components unmountrequestAnimationFrame rate: Default frameRate is optimal for most casesComprehensive API documentation fetched from animejs.com:
| Category | File | Content |
|----------|------|---------|
| Core | docs/getting-started/README.md | Installation, imports, basic usage |
| Core | docs/timer/README.md | createTimer API, playback, callbacks |
| Animation | docs/animation/targets-and-properties.md | CSS selectors, DOM elements, JS objects |
| Animation | docs/animation/tween-values-and-parameters.md | Value types, tween params |
| Animation | docs/animation/keyframes-playback-methods.md | Keyframes, playback controls |
| Timeline | docs/timeline/README.md | Timeline creation, time positioning |
| Animatable | docs/animatable/README.md | Reactive animatable objects |
| Draggable | docs/draggable/README.md | Draggable API, physics settings |
| Layout | docs/layout/README.md | FLIP animations, enter/exit states |
| Scope | docs/scope/README.md | Scoped animations, media queries |
| Events | docs/events/onscroll/README.md | ScrollObserver, thresholds, sync |
| SVG | docs/svg/README.md | morphTo, createDrawable, createMotionPath |
| Text | docs/text/splittext/README.md | Text splitting, lines/words/chars |
| Utilities | docs/utilities/stagger/README.md | Stagger function, grid staggering |
| Utilities | docs/utilities/helpers.md | $(), get(), set(), random, math |
| Easings | docs/easings/README.md | Built-in eases, spring physics |
| WAAPI | docs/web-animation-api/README.md | Hardware acceleration, API differences |
| Engine | docs/engine/README.md | Engine config, time units, FPS |
Native browser Web Animations API documentation from MDN:
| Category | File | Content |
|----------|------|---------|
| Guides | docs/web-animations-api/guides/README.md | Overview, concepts, keyframe formats, tips |
| Interfaces | docs/web-animations-api/interfaces/animation.md | Animation, AnimationEffect, KeyframeEffect |
| Interfaces | docs/web-animations-api/interfaces/timelines.md | AnimationTimeline, DocumentTimeline, ScrollTimeline, ViewTimeline |
| Methods | docs/web-animations-api/methods/README.md | Element.animate(), getAnimations(), events |
For patterns and techniques beyond the API docs:
references/accessibility-and-testing.md - prefers-reduced-motion, TypeScript, testing, common mistakesreferences/native-waapi.md - Native Web Animations API (Element.animate) - timing, keyframes, playback controlsreferences/scroll-animations.md - ScrollObserver patterns and scroll-triggered animationsreferences/svg-animations.md - SVG morphing, path drawing, and motion pathsreferences/advanced-patterns.md - Text animations, draggable elements, layout animationsWorking examples in examples/:
basic-animation.js - Core animation patternstimeline-sequence.js - Timeline orchestrationreact-integration.jsx - React hooks and patternsscroll-reveal.js - Scroll-triggered animationsdevelopment
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.