Animation patterns using Anime.js v4 for UI feedback, transitions, and celebrations. Use when implementing hover effects, transitions, loading animations, or gamification feedback...
Animation system using Anime.js v4 for responsive, accessible UI motion. This skill covers animation patterns, timing, and reduced motion support.
animejs-v4: Complete Anime.js 4.0 API referencejs-micro-utilities: Array utilities like .at(-1) for accessing last elementux-iconography: Icon animation patternsux-accessibility: Reduced motion requirementsimport { animate } from 'animejs';
Note: Project uses import maps to resolve animejs to local node_modules.
The project provides reusable animations in js/utils/animations.js:
import {
shake,
pressEffect,
successBounce,
glow,
DURATION,
EASE
} from '../../utils/animations.js';
const DURATION = {
instant: 100, // Micro-interactions
quick: 200, // Button press, toggles
normal: 300, // Standard transitions
slow: 500, // Page transitions
celebration: 600 // Success animations
};
const EASE = {
snap: 'easeOutQuad', // Quick, snappy feel
smooth: 'easeInOutQuad', // Gentle transitions
bounceOut: 'easeOutBack' // Playful, overshooting
};
For button clicks and taps:
pressEffect(element);
// or
animate(element, {
scale: [1, 0.95, 1],
duration: DURATION.instant,
ease: EASE.snap
});
For completed actions:
successBounce(element);
// or
animate(element, {
scale: [1, 1.1, 1],
duration: DURATION.normal,
ease: EASE.bounceOut
});
For validation failures:
shake(element, { intensity: 6 });
// or
animate(element, {
translateX: [-6, 6, -6, 6, -3, 3, 0],
duration: DURATION.normal,
ease: EASE.snap
});
For achievements or highlights:
glow(element, {
color: 'rgba(74, 222, 128, 0.6)',
intensity: 15
});
// Uses box-shadow animation
For elements requiring attention:
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.attention {
animation: pulse 1.5s ease-in-out infinite;
}
animate(element, {
opacity: [0, 1],
duration: DURATION.normal,
ease: EASE.smooth
});
animate(element, {
translateY: [20, 0],
opacity: [0, 1],
duration: DURATION.normal,
ease: EASE.snap
});
animate(element, {
scale: [0.9, 1],
opacity: [0, 1],
duration: DURATION.quick,
ease: EASE.bounceOut
});
animate('.list-item', {
translateY: [20, 0],
opacity: [0, 1],
delay: (el, i) => i * 50,
duration: DURATION.normal,
ease: EASE.snap
});
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'completed' && newVal !== null) {
this.#animateComplete();
}
}
#animateComplete() {
animate(this, {
scale: [1, 2, 1],
duration: DURATION.normal,
ease: EASE.bounceOut
});
}
#animatePhaseChange() {
animate(this.#container, {
opacity: [1, 0, 1],
duration: DURATION.slow,
ease: EASE.smooth
});
}
#celebrateWord() {
successBounce(this.#wordElement);
glow(this.#wordElement, { color: 'rgba(74, 222, 128, 0.6)' });
}
#animateScore() {
animate(this.#scoreElement, {
scale: [1, 1.2, 1],
duration: DURATION.quick,
ease: EASE.bounceOut
});
}
#celebrateAchievement() {
animate(this.#badge, {
scale: [0, 1.2, 1],
rotate: [0, -10, 10, 0],
duration: DURATION.celebration,
ease: EASE.bounceOut
});
}
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
animate(element, { scale: [1, 1.1, 1] });
} else {
// Instant state change instead
element.classList.add('completed');
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
function safeAnimate(element, props) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// Apply final state immediately
if (props.scale) element.style.transform = `scale(${props.scale.at(-1)})`;
if (props.opacity) element.style.opacity = props.opacity.at(-1);
return;
}
return animate(element, props);
}
For simple state changes, prefer CSS:
.button {
transition:
background-color 0.15s ease,
transform 0.1s ease,
opacity 0.15s ease;
}
.button:hover {
background: var(--color-hover-overlay);
}
.button:active {
transform: scale(0.98);
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--theme-outline);
border-top-color: var(--theme-primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { rotate: 360deg; }
}
.loading-dots span {
animation: bounce 1s ease-in-out infinite;
}
.loading-dots span:nth-child(2) { animation-delay: 0.1s; }
.loading-dots span:nth-child(3) { animation-delay: 0.2s; }
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
transform and opacity for smooth 60fpswill-change sparingly for complex animationswidth, height, margin, padding#animation = null;
disconnectedCallback() {
if (this.#animation) {
this.#animation.pause();
this.#animation = null;
}
}
| Action | Duration | Easing |
|---|---|---|
| Button press | 100ms | easeOutQuad |
| Toggle switch | 150ms | easeOutQuad |
| Dropdown open | 200ms | easeOutQuad |
| Modal appear | 200-300ms | easeOutBack |
| Page transition | 300-500ms | easeInOutQuad |
| Success celebration | 400-600ms | easeOutBack |