Skip to content

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.

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():

Play
Play me
import { createMotionPlayer } from '@andersseen/motion';
const player = createMotionPlayer(document.getElementById('target'));
selectEl.addEventListener('andSelectChange', () => {
player.play(selectEl.value); // any catalog name
});

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:

Replay stagger
Item 1 — 0ms
Item 2 — 80ms
Item 3 — 160ms
Item 4 — 240ms
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 seekersheart-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:

 Like

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');
}
});

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
});

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.