# UI Animation Patterns — Copy-Paste Reference ## Contents - Motion tokens - Micro-interaction (button press/hover) - Enter/exit transition (dialog, toast) - Scroll reveal with IntersectionObserver - Page-load sequence (staggered) - Web Animations API sequencing - Reduced-motion gate - Gotchas ## Motion tokens ```css :root { --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* enter: fast start, soft landing */ --ease-in: cubic-bezier(0.7, 0, 0.84, 0); /* exit: accelerate away */ --dur-fast: 150ms; /* hover, press feedback */ --dur-base: 250ms; /* enters, exits, toggles */ --dur-slow: 450ms; /* scene-level changes */ } ``` ## Micro-interaction (button press/hover) ```css .btn { transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); } .btn:hover { transform: translateY(-1px); } .btn:active { transform: translateY(0) scale(0.98); } ``` ## Enter/exit transition (dialog, toast) Enter with ease-out, exit faster with ease-in. ```css .toast { transition: transform var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out); } .toast[data-state="closed"] { transform: translateY(8px); opacity: 0; transition-duration: var(--dur-fast); transition-timing-function: var(--ease-in); } ``` Animating from `display: none` (modern CSS): ```css dialog[open] { opacity: 1; transform: none; transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), display var(--dur-base) allow-discrete; @starting-style { opacity: 0; transform: translateY(12px); } } ``` ## Scroll reveal with IntersectionObserver Content is fully visible without JS; the class only *enables* the hidden start state. ```html
``` ```css .js [data-reveal] { opacity: 0; transform: translateY(16px); } .js [data-reveal].is-shown { opacity: 1; transform: none; transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out); } ``` ```js document.documentElement.classList.add('js'); const io = new IntersectionObserver((entries) => { for (const e of entries) if (e.isIntersecting) { e.target.classList.add('is-shown'); io.unobserve(e.target); // reveal once } }, { threshold: 0.15 }); // fire when 15% visible document.querySelectorAll('[data-reveal]').forEach(el => io.observe(el)); ``` ## Page-load sequence (staggered) One orchestrated moment: hero elements enter in order, 60ms apart. ```css .hero > * { opacity: 0; transform: translateY(12px); animation: enter var(--dur-slow) var(--ease-out) forwards; animation-delay: calc(var(--i) * 60ms); } @keyframes enter { to { opacity: 1; transform: none; } } ``` ```html

``` ## Web Animations API sequencing For interruption-safe, runtime-computed motion. ```js const panel = document.querySelector('.panel'); const anim = panel.animate( [{ transform: 'translateX(100%)' }, { transform: 'none' }], { duration: 250, easing: 'cubic-bezier(0.16,1,0.3,1)', fill: 'forwards' } ); // interruptible: reverse mid-flight instead of restarting closeBtn.onclick = () => anim.reverse(); await anim.finished; // sequencing point ``` ## Reduced-motion gate ```css @media (prefers-reduced-motion: reduce) { [data-reveal], .hero > * { opacity: 1 !important; transform: none !important; animation: none !important; transition: none !important; } } ``` ```js const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; if (!reduced) el.animate(/* … */); ``` ## Gotchas - `transition: all` invites accidental layout animations and future regressions — always list properties. - `animation-fill-mode: forwards` keeps the element on its final keyframe, which can trap it in a stale state after class changes; prefer transitioning real end states. - `will-change` left permanently on many elements consumes GPU memory — add before heavy animation, remove after. - Staggers multiply duration: 10 items × 100ms delay = a 1s+ sequence. Cap total sequence time near 700ms. - `height: auto` can't transition; animate `grid-template-rows: 0fr → 1fr` on a wrapper, or use WAAPI with measured pixel values. - IntersectionObserver with `threshold: 1.0` never fires on elements taller than the viewport.