Vanilla motion values

Everything the use* motion-value hooks do, without a component: create values in module scope, .svelte.ts stores, or event handlers, and bind them straight to plain DOM elements with styleEffect — mirroring Motion’s “vanilla JavaScript” API.

// stores/scroll.svelte.ts — impossible with useMotionValue
import { motionValue } from '@humanspeak/svelte-motion'

export const scrollProgress = motionValue(0)
// stores/scroll.svelte.ts — impossible with useMotionValue
import { motionValue } from '@humanspeak/svelte-motion'

export const scrollProgress = motionValue(0)
mode · live running

$statetoMotionValuemapValuespringValuestyleEffect

Hooks vs vanilla

Both layers return the same augmented value: a real motion-dom MotionValue (passes isMotionValue, works with animate()) with a rune-reactive .current and a store .subscribe. The only difference is lifecycle:

use* hooksVanilla factories
Where they workComponent init onlyAnywhere
CleanupAutomatic on unmountManual .destroy()
ExamplesuseMotionValue, useSpring, useTransformmotionValue, springValue, transformValue, mapValue

Values that live for the app’s lifetime (module-scope stores) never need destroying. Values created per-interaction should be destroyed when done — a getter-bridged value holds a live Svelte effect until then.

The factories

import { mapValue, motionValue, springValue, transformValue } from '@humanspeak/svelte-motion'

const x = motionValue(0)

// Range mapping — numbers, colors, unit strings
const opacity = mapValue(x, [0, 200], [1, 0])
const color = mapValue(x, [0, 200], ['#ff0088', '#00ccff'])

// Springs toward a source (values or unit strings)
const smooth = springValue(x, { stiffness: 300, damping: 30 })

// Computed from .get() reads — dependencies auto-tracked
const label = transformValue(() => `${Math.round(x.get())}px`)
import { mapValue, motionValue, springValue, transformValue } from '@humanspeak/svelte-motion'

const x = motionValue(0)

// Range mapping — numbers, colors, unit strings
const opacity = mapValue(x, [0, 200], [1, 0])
const color = mapValue(x, [0, 200], ['#ff0088', '#00ccff'])

// Springs toward a source (values or unit strings)
const smooth = springValue(x, { stiffness: 300, damping: 30 })

// Computed from .get() reads — dependencies auto-tracked
const label = transformValue(() => `${Math.round(x.get())}px`)

springValue and mapValue accept any source kind: a MotionValue, a Svelte readable store, or a reactive getter.

toMotionValue — the rune bridge

Turn any Svelte state into a motion value. The getter form tracks $state / $derived reads (and other augmented values’ .current):

<script lang="ts">
    import { styleEffect, toMotionValue } from '@humanspeak/svelte-motion'

    let progress = $state(0)
    const opacity = toMotionValue(() => progress / 100)

    let el = $state<HTMLElement | null>(null)
    $effect(() => {
        if (!el) return
        const stop = styleEffect(el, { opacity })
        return () => stop()
    })
</script>

<input type="range" bind:value={progress} />
<div bind:this={el}>Fades with the slider</div>
<script lang="ts">
    import { styleEffect, toMotionValue } from '@humanspeak/svelte-motion'

    let progress = $state(0)
    const opacity = toMotionValue(() => progress / 100)

    let el = $state<HTMLElement | null>(null)
    $effect(() => {
        if (!el) return
        const stop = styleEffect(el, { opacity })
        return () => stop()
    })
</script>

<input type="range" bind:value={progress} />
<div bind:this={el}>Fades with the slider</div>

Passing an existing MotionValue returns the same value; passing a readable store returns a mirroring value. Getter-driven updates flush on Svelte’s effect schedule (a microtask), matching template timing.

The hooks understand getters too — useSpring(() => target) and useTransform(() => count, [0, 10], [0, 100]) track rune state the same way.

Element effects

Bind values directly to elements — no motion component in sight:

import { attrEffect, styleEffect, svgEffect } from '@humanspeak/svelte-motion'

const stop = styleEffect('.box', { x, opacity })      // inline styles
attrEffect(input, { value: count })                    // attributes
svgEffect(path, { pathLength: progress })              // SVG attributes
import { attrEffect, styleEffect, svgEffect } from '@humanspeak/svelte-motion'

const stop = styleEffect('.box', { x, opacity })      // inline styles
attrEffect(input, { value: count })                    // attributes
svgEffect(path, { pathLength: progress })              // SVG attributes

Each returns an unbind function. Import them from this package (not motion) so both raw and augmented values type-check.

Scheduler, generators, config

The rest of Motion’s vanilla toolkit re-exports directly:

import {
    cancelFrame,
    frame,            // frameloop scheduler: frame.read / frame.update / frame.render
    MotionGlobalConfig, // e.g. skipAnimations for tests
    scrollInfo,
    spring            // spring generator for WAAPI / custom drivers
} from '@humanspeak/svelte-motion'

frame.render(() => {
    // runs on Motion's render step this frame
})
import {
    cancelFrame,
    frame,            // frameloop scheduler: frame.read / frame.update / frame.render
    MotionGlobalConfig, // e.g. skipAnimations for tests
    scrollInfo,
    spring            // spring generator for WAAPI / custom drivers
} from '@humanspeak/svelte-motion'

frame.render(() => {
    // runs on Motion's render step this frame
})

Plus the steps, mirrorEasing, and reverseEasing easing helpers alongside the existing easing exports.

Related


Based on Motion’s vanilla motion value API.