Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Motion values

Drive styles from Motion's motion values — scroll progress, springs and derived values — without re-running Svelte code.

Overview

A motion value holds a value that changes over time — a scroll position, a pointer coordinate, a spring — and notifies whatever renders it, outside Svelte’s reactivity. Mercury renders motion values on an element through the style option:

<script lang="ts">
	import { mercury } from '@omicrxn/mercury';
	import { motionValue } from 'motion';

	const x = motionValue(0);
</script>

<div {@attach mercury({ style: { x } })} />
<button onclick={() => x.set(x.get() + 50)}>Move</button>

style is Motion’s style prop with motion values: transforms (x, scale, rotate…), any CSS property, and CSS variables. Static styles belong in Svelte’s own style attribute. A value in style can still be animated by animate or a gesture.

Motion’s value helpers work as they do in React — they are the vanilla versions of its hooks:

motion/react Motion / Mercury
useMotionValue(0) motionValue(0)
useTransform(() => x.get() * 2) transformValue(() => x.get() * 2)
useTransform(x, [0, 1], [0, 100]) mapValue(x, [0, 1], [0, 100])
useSpring(x) springValue(x)
useMotionTemplate transformValue(() => \blur(${blur.get()}px)`)`
useMotionValueEvent(x, 'change', f) x.on('change', f) (returns the unsubscribe)
useScroll() scrollValues() from Mercury
useAnimationFrame(f) frame.update(f, true), stopped with cancelFrame(f)
useReducedMotion() prefersReducedMotion.current from svelte/motion

Everything but scrollValues is imported from motion.

Text

Render a motion value as an element’s text with text (Motion’s <motion.span>{value}</motion.span>):

<script lang="ts">
	import { animate, motionValue, transformValue } from 'motion';

	const count = motionValue(0);
	const rounded = transformValue(() => Math.round(count.get()));
	$effect(() => {
		const controls = animate(count, 100, { duration: 2 });
		return () => controls.stop();
	});
</script>

<span {@attach mercury({ text: rounded })}>0</span>

Scroll

scrollValues() is Motion’s useScroll(): it returns motion values for the scroll position and progress (0–1) of the page, a scrollable container, or a target element’s progress through it. Call it in a component’s <script>; it stops tracking when the component is destroyed.

<script lang="ts">
	import { mercury, scrollValues } from '@omicrxn/mercury';
	import { springValue } from 'motion';

	const { scrollYProgress } = scrollValues();
	const scaleX = springValue(scrollYProgress, { stiffness: 300, damping: 40 });
</script>

<div class="progress-bar" {@attach mercury({ style: { scaleX } })}></div>
<script lang="ts">
	import { mercury, scrollValues } from '@omicrxn/mercury';
	import { springValue, transformValue } from 'motion';

	let container = $state<HTMLElement>();
	const { scrollYProgress } = scrollValues({ container: () => container });
	// Motion's useSpring / useTransform: smooth the progress and derive a label from it.
	const progress = springValue(scrollYProgress, { stiffness: 300, damping: 40 });
	const percent = transformValue(() => `${Math.round(scrollYProgress.get() * 100)}%`);
</script>

<div class="flex w-full max-w-[400px] flex-col gap-2">
	<div class="flex items-center gap-3">
		<div class="h-2 flex-1 overflow-hidden rounded-full bg-demo-container">
			<div
				class="h-full origin-left rounded-full bg-demo-accent"
				style="transform: scaleX(0)"
				{@attach mercury({ style: { scaleX: progress } })}
			></div>
		</div>
		<span class="w-10 text-right text-sm tabular-nums" {@attach mercury({ text: percent })}>0%</span
		>
	</div>
	<!-- svelte-ignore a11y_no_noninteractive_tabindex (The scroll region needs keyboard focus to scroll.) -->
	<div
		bind:this={container}
		role="region"
		aria-label="Scrollable article"
		tabindex="0"
		class="h-56 overflow-y-auto overscroll-contain rounded-md border border-border p-4 text-sm leading-relaxed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-demo-accent"
	>
		{#each { length: 8 }, i}
			<p class="mb-4">
				Paragraph {i + 1}. Mercury's <code>scrollValues()</code> exposes Motion's scroll position
				and progress as motion values, which <code>style</code> renders without re-running any Svelte
				code.
			</p>
		{/each}
	</div>
</div>

Options:

  • container: the scrollable element. Defaults to the page.
  • target: track this element’s progress through the container instead.
  • offset: Motion’s scroll offsets, e.g. ['start end', 'end start'] — from the target’s top meeting the container’s bottom, to its bottom meeting the container’s top.
  • axis: 'y' (default) or 'x'.

container and target take an element, a { current } ref, or a getter — () => element — so they can point at elements bound with bind:this; tracking starts once they’re set.

Returned values: scrollX, scrollY (pixels) and scrollXProgress, scrollYProgress (0–1).

For animations that play when an element scrolls into view, see whileInView. For scroll-linked animations without motion values, Motion’s scroll() works on any element.

Was this page helpful?