Motion — Recipes
Practical patterns built from the two lower-level APIs — the
and-motion attribute and the
imperative player. Everything below is grounded in
the real engine (no extra helpers); the live demos on this page drive
createMotionPlayer directly, exactly as your own code would.
Live playground
Section titled “Live playground”Pick any name from the animation catalog
and replay it on demand. This is just a createMotionPlayer bound to one target
element, with the Select’s value passed straight to
play():
import { createMotionPlayer } from '@andersseen/motion';
const player = createMotionPlayer(document.getElementById('target'));
selectEl.addEventListener('andSelectChange', () => { player.play(selectEl.value); // any catalog name});Staggered list reveal
Section titled “Staggered list reveal”Give each item its own --and-motion-delay (as an inline custom property) and
play the same animation on all of them at once — the per-element delay does
the staggering. play() reads --and-motion-delay off the element’s computed
style, and it survives replays because the player only clears the animation-*
shorthand properties, never your custom property:
import { createMotionPlayer } from '@andersseen/motion';
const items = [...document.querySelectorAll('.item')];
// Delay is a plain inline custom property — assign it once…items.forEach((el, i) => { el.style.setProperty('--and-motion-delay', `${i * 80}ms`);});
// …then play the same animation on all of them.const players = items.map(createMotionPlayer);function reveal() { players.forEach(p => p.play('fade-up'));}Prefer the declarative route if the list is static and should reveal on
scroll rather than on demand — set the delays as attributes and let the enter
trigger fire each one as it scrolls into view:
<div and-motion="fade-up" and-motion-trigger="enter" and-motion-delay="0ms"> Item 1</div><div and-motion="fade-up" and-motion-trigger="enter" and-motion-delay="80ms"> Item 2</div><div and-motion="fade-up" and-motion-trigger="enter" and-motion-delay="160ms"> Item 3</div>Attention on interaction
Section titled “Attention on interaction”Attention seekers —
heart-beat, shake-x, tada, pulse, … — play in place on an
already-visible element, so they’re a natural fit for feedback: a “liked” tap, a
failed-validation nudge, a “copied!” confirmation. Here a tap replays
heart-beat imperatively:
For a purely declarative version — no script at all — reach for the tap
trigger, which fires the animation on pointerdown and resets on release:
<button> <span and-motion="heart-beat" and-motion-trigger="tap">♥</span> Like</button>A validation nudge is the same idea with shake-x, played from your submit
handler when the field is invalid:
const player = createMotionPlayer(emailField);
form.addEventListener('submit', e => { if (!emailField.checkValidity()) { e.preventDefault(); player.play('shake-x'); }});Scroll-triggered reveal
Section titled “Scroll-triggered reveal”The enter trigger uses an IntersectionObserver, so content animates the
first time it scrolls into view. By default each element animates once
(once: true); override per element with and-motion-once="false" to
re-animate every time it re-enters the viewport:
<!-- Reveals once, the first time it's seen --><section and-motion="fade-up" and-motion-trigger="enter">…</section>
<!-- Re-animates every time it scrolls back into view --><div and-motion="zoom-in" and-motion-trigger="enter" and-motion-once="false"> …</div>Tune the observer globally through
initMotion — a larger
threshold waits until more of the element is visible before firing, and a
negative rootMargin delays the trigger until the element is further up the
viewport:
import { initMotion } from '@andersseen/motion';
initMotion({ threshold: 0.25, // fire when 25% visible rootMargin: '0px 0px -10% 0px', // …and 10% above the bottom edge});Coordinating with SPA navigation
Section titled “Coordinating with SPA navigation”initMotion() scans once when called. In a single-page app that swaps
content on route changes, run the cleanup returned by initMotion() on the way
out and call it again on the way in — or hold a
MotionController and call
scan() after each navigation to wire up newly-rendered [and-motion]
elements:
import { MotionController } from '@andersseen/motion';
const motion = new MotionController();
router.afterEach(() => { motion.scan(); // idempotent — already-wired nodes are skipped});
// On full teardown:motion.destroy();Imperative createMotionPlayer instances aren’t tied to the scan at all — they
animate whatever element you hand them, whenever you call play(). If that
element is unmounted, call player.destroy() to drop its listener.