UX Animation & Motion Skill
Animation system using Anime.js v4 for responsive, accessible UI motion. This skill covers animation patterns, timing, and reduced motion support.
Related Skills
-
animejs-v4 : Complete Anime.js 4.0 API reference
-
js-micro-utilities : Array utilities like .at(-1) for accessing last element
-
ux-iconography : Icon animation patterns
-
ux-accessibility : Reduced motion requirements
Animation Import
import { animate } from 'animejs';
Note: Project uses import maps to resolve animejs to local node_modules.
Animation Utilities
The project provides reusable animations in js/utils/animations.js :
import { shake, pressEffect, successBounce, glow, DURATION, EASE } from '../../utils/animations.js';
Duration Constants
const DURATION = { instant: 100, // Micro-interactions quick: 200, // Button press, toggles normal: 300, // Standard transitions slow: 500, // Page transitions celebration: 600 // Success animations };
Easing Functions
const EASE = { snap: 'easeOutQuad', // Quick, snappy feel smooth: 'easeInOutQuad', // Gentle transitions bounceOut: 'easeOutBack' // Playful, overshooting };
Animation Patterns
Press Effect (Tactile Feedback)
For button clicks and taps:
pressEffect(element); // or animate(element, { scale: [1, 0.95, 1], duration: DURATION.instant, ease: EASE.snap });
Success Bounce
For completed actions:
successBounce(element); // or animate(element, { scale: [1, 1.1, 1], duration: DURATION.normal, ease: EASE.bounceOut });
Shake (Error/Invalid)
For validation failures:
shake(element, { intensity: 6 }); // or animate(element, { translateX: [-6, 6, -6, 6, -3, 3, 0], duration: DURATION.normal, ease: EASE.snap });
Glow Effect
For achievements or highlights:
glow(element, { color: 'rgba(74, 222, 128, 0.6)', intensity: 15 }); // Uses box-shadow animation
Pulse (Attention)
For elements requiring attention:
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.attention { animation: pulse 1.5s ease-in-out infinite; }
Transition Patterns
Fade In
animate(element, { opacity: [0, 1], duration: DURATION.normal, ease: EASE.smooth });
Slide In
animate(element, { translateY: [20, 0], opacity: [0, 1], duration: DURATION.normal, ease: EASE.snap });
Scale In
animate(element, { scale: [0.9, 1], opacity: [0, 1], duration: DURATION.quick, ease: EASE.bounceOut });
Staggered List
animate('.list-item', { translateY: [20, 0], opacity: [0, 1], delay: (el, i) => i * 50, duration: DURATION.normal, ease: EASE.snap });
State Change Animations
Attribute Change Response
attributeChangedCallback(name, oldVal, newVal) { if (name === 'completed' && newVal !== null) { this.#animateComplete(); } }
#animateComplete() { animate(this, { scale: [1, 2, 1], duration: DURATION.normal, ease: EASE.bounceOut }); }
Phase Transition
#animatePhaseChange() { animate(this.#container, { opacity: [1, 0, 1], duration: DURATION.slow, ease: EASE.smooth }); }
Game Feedback Animations
Word Completion
#celebrateWord() { successBounce(this.#wordElement); glow(this.#wordElement, { color: 'rgba(74, 222, 128, 0.6)' }); }
Score Update
#animateScore() { animate(this.#scoreElement, { scale: [1, 1.2, 1], duration: DURATION.quick, ease: EASE.bounceOut }); }
Achievement Unlock
#celebrateAchievement() { animate(this.#badge, { scale: [0, 1.2, 1], rotate: [0, -10, 10, 0], duration: DURATION.celebration, ease: EASE.bounceOut }); }
Reduced Motion Support
Check Preference
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'); }
CSS Fallback
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }
Safe Animation Wrapper
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);
}
CSS Transitions
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); }
Loading Animations
Spinner
.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; } }
Dots
.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); } }
Performance Guidelines
Do
-
Use transform and opacity for smooth 60fps
-
Keep animations under 300ms for interactions
-
Use will-change sparingly for complex animations
-
Cancel animations when element unmounts
Don't
-
Animate width , height , margin , padding
-
Use long durations for frequent interactions
-
Chain many sequential animations
-
Animate elements off-screen
Cleanup
#animation = null;
disconnectedCallback() { if (this.#animation) { this.#animation.pause(); this.#animation = null; } }
Animation Timing Reference
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