svelte-attach-motion
v0.1.0
Published
Motion animations for Svelte 5 as attachments: animate, inView, scroll, hover and press, with reduced-motion support built in.
Maintainers
Readme
svelte-attach-motion
Motion animations for Svelte 5, as attachments.
Docs & demo: ds2l9pktw39ce.cloudfront.net
No wrapper components and no <motion.div>. Add animation to the elements you already have, with automatic cleanup and prefers-reduced-motion support built in.
<script>
import { animate, inView, hover, press } from 'svelte-attach-motion';
</script>
<h1 {@attach animate({ opacity: [0, 1], y: [16, 0] })}>Hello</h1>
<section {@attach inView({ opacity: 1 }, { initial: { opacity: 0 } })}>…</section>
<button {@attach press({ scale: 0.95 })} {@attach hover({ y: -2 })}>Save</button>Install
npm install svelte-attach-motion motionRequires Svelte 5.29+ (attachments) and Motion 13. Motion is a peer dependency, so your app controls its version.
API
| Attachment | What it does |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| animate(keyframes, options?) | Animates on mount, and again whenever reactive values in keyframes change. |
| inView(keyframes, options?) | Animates when the element enters the viewport. once: false replays each time. |
| scrollAnimate(keyframes, options?) | Links an animation to the element's scroll position. |
| scrollProgress(callback, options?) | Calls callback(progress) with 0–1 as the element (or page) scrolls. |
| hover(keyframes, options?) | Animates while a mouse hovers the element, then returns to rest. Ignores touch. |
| press(keyframes, options?) | Animates while pressed, by pointer or keyboard (Enter), then returns to rest. |
All animating attachments accept:
transition: Motion transition options (duration,delay,ease,type: 'spring', ...).reducedMotion:'user'(default),'always'or'never'. See below.
hover and press also accept rest, the values to return to when the gesture ends. You rarely need it: transform shorthands return to their identity (scale: 1, x: 0, ...) and other properties return to their computed style at mount time.
Reactive animations
Attachments re-run when the state they read changes, so this animates the knob every time on flips:
<script>
let on = $state(false);
</script>
<button onclick={() => (on = !on)}>
<span {@attach animate({ x: on ? 40 : 0 }, { transition: { type: 'spring' } })}></span>
</button>Reduced motion
By default every attachment respects the user's prefers-reduced-motion setting:
animate,inView,hoverandpressstill reach their final state, so content is never left hidden, but instantly.scrollAnimateis skipped entirely, because scroll-linked motion is a common trigger for discomfort.scrollProgressonly reports numbers, so it is left to your callback.
Use reducedMotion: 'never' only for motion that is essential to understanding the UI.
No flash on server-rendered pages
Attachments run once JavaScript has loaded. On a server-rendered or prerendered page, an entrance animation would otherwise show the element, hide it when the attachment mounts, then animate it in.
Import the stylesheet once and add data-reveal to entrance animations:
<!-- +layout.svelte -->
<script>
import 'svelte-attach-motion/reveal.css';
</script><h1 data-reveal {@attach animate({ opacity: [0, 1], y: [12, 0] })}>…</h1>
<section data-reveal {@attach inView({ opacity: 1, y: 0 }, { initial: { opacity: 0, y: 24 } })}>
…
</section>- Elements are hidden from the first paint only when the browser can run scripts
(
@media (scripting: enabled)), so content stays visible without JavaScript. animateandinViewtake over when they mount and remove the attribute after their first frame. OninView,initialdefaults to{ opacity: 0 }fordata-revealelements.- If the JavaScript never loads, a CSS fail-safe shows the element after 3 seconds.
- Only use it on entrance animations that end at
opacity: 1.
Name clashes with Motion
If you also import from motion directly, alias one side:
import { animate as motionAnimate } from 'motion';
import { animate } from 'svelte-attach-motion';Why attachments?
Svelte 5 attachments are functions that run when an element mounts and clean up when it unmounts, and they re-run when their reactive inputs change. That maps directly onto Motion's vanilla API, which returns a cleanup function for everything. The result is a thin layer: there are no components to learn and nothing changes about your markup.
Development
pnpm install
pnpm dev # docs + demo site at localhost:5173
pnpm check # svelte-check (TypeScript)
pnpm lint # Prettier + ESLint
pnpm test:unit # Vitest
pnpm test:e2e # Playwright (run `pnpm exec playwright install chromium` once first)
pnpm build # static site to build/ and the package to dist/Project layout:
src/lib/ the published package
attachments/ animate, inView, scroll*, hover/press
internal/ pure helpers with unit tests
src/routes/ docs site and the "Spot the phish" demo
e2e/ Playwright tests (animations, gestures, reduced motion, demo)