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.