@rootenginear/svelte-action-motionone

npm jsr GitHub License GitHub

Unofficial Svelte Action for Motion One animation library

npm install @rootenginear/svelte-action-motionone
deno add jsr:@rootenginear/svelte-action-motionone

Table of Contents

  1. The Idea
  2. use:inView
  3. use:scroll
  4. use:containerScroll
  5. use:scrollInView
  6. use:hover
  7. use:press
  8. More Examples
    1. Staggered Animation
    2. Timeline Sequences
    3. Enable/Disable Animation
  9. Best Practices

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
Hello!
<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

Scroll Me! → 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
	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

Ooh!
<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
Hover me!
<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
Press me!
<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>