@rootenginear/svelte-action-motionone
Unofficial Svelte Action for Motion One animation library
npm install @rootenginear/svelte-action-motionone
deno add jsr:@rootenginear/svelte-action-motionone
Table of Contents
The Idea
Basically it's the same for Motion, just omit the node and passing other parameters as an array.
Motion's API:
inView(elementOrSelector, onStart, options)
press(elementOrSelector, onPressStart, options)
hover(elementOrSelector, onHoverStart, options)
scroll(onScroll, options)
svelte-action-motionone
<div use:inView={[onStart, options]} />
<div use:press={[onPressStart, options]} />
<div use:hover={[onHoverStart, options]} />
<div use:scroll={[onScroll, options]} />
You can also pass other parameters as a function that accepts node
as
an argument. You can use node
to reference the action's DOM node.
<div use:inView={(node) => [onStart, options]} />
<div use:press={(node) => [onPressStart, options]} />
<div use:hover={(node) => [onHoverStart, options]} />
<div use:scroll={(node) => [onScroll, options]} />
Compatibility: Svelte 4, Motion 12
use:inView
https://motion.dev/docs/inview
<div
use:inView={[
(el) => {
animate(
el,
{ scale: [0, 1], opacity: [0, 1] },
{
type: spring,
bounce: 0.3,
duration: 1
}
);
},
{ amount: 1 }
]}
style="background:#FFDC00;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold"
>
Hello!
</div>
use:scroll
https://motion.dev/docs/scroll
<div
use:scroll={(node) => [
animate(node, { transform: ['scaleX(0)', 'scaleX(1)'] }, { ease: 'linear' })
]}
style="position:fixed;top:0;left:0;width:100%;height:4px;z-index:50;background:#FF4136;transform-origin:left"
/>
You can see the example at the top of the page (the progress bar!)
use:containerScroll
Watch the scroll of that element. use:containerScroll
is a shortcut for:
<div use:scroll={(node) => [/* ... */, { container: node }]} />
Example
<div
use:containerScroll={(node) => [
animate(node.children[0], {
backgroundColor: ['#FF4136', '#FFDC00']
}),
{
axis: 'x'
}
]}
style="overflow-x:auto;user-select:none"
>
<div style="width:max-content;padding:16px">
<span style="font-weight:bold">Scroll Me! →</span> Lorem ipsum dolor sit amet consectetur, adipisicing
elit. Incidunt provident odit voluptatibus magni quae autem unde sed libero voluptatum, et quibusdam
tempore voluptas harum natus cum mollitia soluta perferendis ut.
</div>
</div>
use:scrollInView
Watch the progress of that element in viewport. use:scrollInView
is a shortcut
for:
<div use:scroll={(node) => [/* ... */, { target: node }]} />
Example
<div
use:scrollInView={(node) => [
animate(node, { scale: [0, 1, 0] }),
{
offset: ['0 1', '1 0']
}
]}
style="background:#FFDC00;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold"
>
Ooh!
</div>
use:hover
https://motion.dev/docs/hover
<div
use:hover={[
(el) => {
animate(el, { scale: 0.8 }, { type: spring, bounce: 0.6, duration: 0.5 });
return () => animate(el, { scale: 1 }, { type: spring, bounce: 0.6, duration: 0.5 });
}
]}
style="background:#FF4136;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold;user-select:none"
>
Hover me!
</div>
use:press
https://motion.dev/docs/press
<div
use:press={[
(el) => {
animate(el, { scale: 0.8 });
return () => animate(el, { scale: 1 }, { type: spring, bounce: 0.6, duration: 0.5 });
}
]}
style="background:#FFDC00;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold;user-select:none"
>
Press me!
</div>
More Examples
Staggered Animation
https://motion.dev/docs/stagger
<div
style="display:flex;gap:8px"
use:inView={[
(el) => {
animate(
[...el.children],
{ scale: [0, 1] },
{ duration: 0.5, delay: stagger(0.2), type: spring, bounce: 0.3 }
);
}
]}
>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FF4136"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FF851B"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FFDC00"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#2ECC40"></div>
</div>
Timeline Sequences
https://motion.dev/docs/animate#timeline-sequences
<div
style="display:flex;gap:8px"
use:inView={[
(el) => {
const children = [...el.children];
animate([
[children[0], { y: [100, 0], opacity: [0, 100] }, { duration: 0.5, at: '-0.3' }],
[children[1], { rotate: [90, 0], opacity: [0, 100] }, { duration: 0.5, at: '-0.3' }],
[children[2], { x: [-100, 0], opacity: [0, 100] }, { duration: 0.5, at: '-0.3' }],
[children[3], { scale: [0, 1], opacity: [0, 100] }, { duration: 0.5, at: '-0.3' }]
]);
}
]}
>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FF4136"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FF851B"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#FFDC00"></div>
<div style="height:64px;flex:1 1 0%;min-width:0;background:#2ECC40"></div>
</div>
Enable/Disable Animation
<script>
let enabled = true;
</script>
<label><input type="checkbox" bind:checked={enabled} /> Enable Animation</label>
<div
use:containerScroll={enabled
? (node) => [
animate(node.children[1], {
rotate: [0, 360],
x: ['-50%', '-50%'],
y: ['-50%', '-50%']
}),
{
axis: 'x'
}
]
: [() => {}]}
style="overflow-x:auto;user-select:none;position:relative"
>
<div style="width:200%;height:96px" />
<div
style="position:absolute;width:64px;height:64px;background:#FF4136;border-radius:16px;left:100%;top:50%"
/>
</div>
You can use this to change/disable the animation to user preference.
<script lang="ts">
import { onMount } from 'svelte';
let isUserPreferringReducedMotion = true;
onMount(() => {
window.matchMedia('(prefers-reduced-motion)').addEventListener('change', (e) => {
isUserPreferringReducedMotion = e.matches;
});
});
</script>
<!-- Use `isUserPreferringReducedMotion` to conditionally enable animation -->
Best Practices
To improve code readability, you can extract animation options into a file somewhere in your utils or as a const in the script section, then reusing them in the template. This will help you to avoid animation options plaguing in the template.
If you are using TypeScript, you can import InViewActionParams
,
ScrollActionParams
, HoverActionParams
and
PressActionParams
to type your option object.
<script lang="ts">
import { inView, type InViewActionParams } from '@rootenginear/svelte-action-motionone';
import { animate } from 'motion';
const fadeInView = [
(el) => {
animate(el, { opacity: [0, 1] });
},
{ amount: 1 }
] satisfies InViewActionParams;
</script>
<div
use:inView={fadeInView}
style="background:#FFDC00;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold"
>
Hello!
</div>
Gotcha: If your animation option is reactive, meaning that you will
enable/disable it or switch to other animation, it should be in a
reactive statement
(or Svelte 5
$derived
).
<script lang="ts">
import { inView, type InViewActionParams } from '@rootenginear/svelte-action-motionone';
import { animate } from 'motion';
import { onMount } from 'svelte';
let isUserPreferringReducedMotion = true;
onMount(() => {
window.matchMedia('(prefers-reduced-motion)').addEventListener('change', (e) => {
isUserPreferringReducedMotion = e.matches;
});
});
$: fadeInView = (
isUserPreferringReducedMotion
? [() => {}]
: [
(el) => {
animate(el, { opacity: [0, 1] });
},
{ amount: 1 }
]
) satisfies InViewActionParams;
</script>
<div
use:inView={fadeInView}
style="background:#FFDC00;padding:16px;border-radius:16px;text-align:center;font-size:32px;font-weight:bold"
>
Hello!
</div>