svelte-attach-motion

Motion for Svelte 5, one attachment at a time.

svelte-attach-motion wraps Motion's animation engine in Svelte attachments. No wrapper components, no <motion.div>. Add animation to the elements you already have, with automatic cleanup and reduced-motion support built in.

npm install svelte-attach-motion motion

animate

Animates an element when it mounts. Pass an array to set start and end values.

<div data-reveal {@attach animate(
  { opacity: [0, 1], y: [24, 0] },
  { transition: { duration: 0.5 } }
)} />

animate + $state

Attachments re-run when the state they read changes, so the element animates to each new value from wherever it currently is.

<script>
  let on = $state(false);
</script>

<button onclick={() => (on = !on)}>
  <span {@attach animate(
    { x: on ? 40 : 0 },
    { transition: { type: 'spring', bounce: 0.35 } }
  )} />
</button>

hover

Animates while a mouse hovers the element and returns to its resting values afterwards. Touch input is ignored, so there are no "stuck" hover states on phones.

Invoice #1842 Due Mar 12 · $240.00
<article {@attach hover(
  { y: -6, boxShadow: '0 12px 24px rgb(0 0 0 / 0.15)' }
)}>…</article>

press

Animates while the element is pressed, by pointer or keyboard. Try focusing the button and holding Enter.

<button {@attach press({ scale: 0.92 })}>Save draft</button>

inView

Animates when the element scrolls into view. Use initial for the hidden state and once: false to replay every time.

  • Inbox
  • Drafts
  • Shared with me
  • Archive
  • Trash
<li data-reveal {@attach inView(
  { opacity: 1, x: 0 },
  { initial: { opacity: 0, x: -32 }, once: false, amount: 0.6 }
)}>…</li>

scrollAnimate

Links an animation to scroll position. The shape rotates as it travels through the viewport. Scroll-linked motion is skipped entirely when the user prefers reduced motion.

<div {@attach scrollAnimate(
  { rotate: [-12, 12], scale: [0.8, 1.1] },
  { offset: ['start end', 'end start'] }
)} />

Reduced motion by default

Every attachment respects prefers-reduced-motion. Animations still reach their final state, so nothing is left hidden, but instantly. Try it on the demos above:

Reduced motion mode
<div {@attach animate(keyframes, { reducedMotion: 'user' })} />
<!-- 'user' (default) | 'always' | 'never' -->

No flash on server-rendered pages

Attachments only run once JavaScript loads, so a prerendered element would first appear, then hide, then animate in. Import reveal.css once and add data-reveal to entrance animations: the element starts hidden only when the browser can run scripts, the attachment takes over when it mounts, and a CSS fail-safe shows it anyway if the JavaScript never arrives.

<!-- +layout.svelte -->
<script>
  import 'svelte-attach-motion/reveal.css';
</script>

<!-- any entrance animation on a server-rendered page -->
<h1 data-reveal {@attach animate({ opacity: [0, 1], y: [12, 0] })}>…</h1>