useTransform

useTransform creates a MotionValue derived from another motion value (or any Svelte readable). It supports two forms:

  • Mapping form: Map a numeric source across input/output ranges with options like clamp, ease, and mixer.
  • Compute form: Recompute from a function whose MotionValue reads are auto-tracked. Plus single-MV and multi-MV transformer forms, and a multi-output mapping form.

The returned value is a real motion-dom MotionValue augmented with a $state-backed .current getter and a Svelte readable .subscribe shim — read it via transformed.current in templates, $transformed for store-style consumers, or transformed.get() in imperative code.

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

  // Time source that ticks every frame
  const time = useTime()

  // Map 0..4000ms -> 0..360deg (unclamped to allow wrap-around)
  const rotate = useTransform(time, [0, 4000], [0, 360], { clamp: false })
</script>

<div style="transform: rotate({rotate.current}deg)">Rotating</div>
<script lang="ts">
  import { useTime, useTransform } from '@humanspeak/svelte-motion'

  // Time source that ticks every frame
  const time = useTime()

  // Map 0..4000ms -> 0..360deg (unclamped to allow wrap-around)
  const rotate = useTransform(time, [0, 4000], [0, 360], { clamp: false })
</script>

<div style="transform: rotate({rotate.current}deg)">Rotating</div>
mode · live running open
// html content 000 / 100
0
source: animate() onUpdate 0 → 100 / 5s

Usage

Mapping form

Map a numeric source across input/output ranges. You can shape interpolation with ease, clamp input to segment bounds with clamp, and provide a custom mixer for non-numeric outputs.

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

  const time = useTime()

  // Progress cycles 0..1 every 2s
  const progress = useTransform(time, [0, 2000], [0, 1], { clamp: false })

  // Map progress to degrees
  const degrees = useTransform(progress, [0, 1], [0, 360])
</script>

<div style="transform: rotate({$degrees}deg)">↻</div>
<script lang="ts">
  import { useTime, useTransform } from '@humanspeak/svelte-motion'

  const time = useTime()

  // Progress cycles 0..1 every 2s
  const progress = useTransform(time, [0, 2000], [0, 1], { clamp: false })

  // Map progress to degrees
  const degrees = useTransform(progress, [0, 1], [0, 360])
</script>

<div style="transform: rotate({$degrees}deg)">↻</div>

With easing

Provide a single easing or one per segment.

<script lang="ts">
  import { useTime, useTransform } from '@humanspeak/svelte-motion'
  const easeIn = (t: number) => t * t

  const time = useTime()
  const size = useTransform(time, [0, 1000, 2000], [0.9, 1.1, 0.9], { ease: [easeIn, easeIn] })
</script>

<div style="transform: scale({$size})">Pulsing</div>
<script lang="ts">
  import { useTime, useTransform } from '@humanspeak/svelte-motion'
  const easeIn = (t: number) => t * t

  const time = useTime()
  const size = useTransform(time, [0, 1000, 2000], [0.9, 1.1, 0.9], { ease: [easeIn, easeIn] })
</script>

<div style="transform: scale({$size})">Pulsing</div>

Non-numeric outputs with mixer

For non-numeric outputs, pass a mixer(from, to) that returns an interpolator (t) => value.

<script lang="ts">
  import { useTransform } from '@humanspeak/svelte-motion'
  import { writable } from 'svelte/store'

  // Source 0..1
  const src = writable(0)

  // Simple discrete color mixer
  const stepColor = (from: string, to: string) => (t: number) => (t < 0.5 ? from : to)

  const color = useTransform(src, [0, 1], ['red', 'blue'], { mixer: stepColor })
</script>

<div style="background: {$color}; width: 80px; height: 24px;" />
<script lang="ts">
  import { useTransform } from '@humanspeak/svelte-motion'
  import { writable } from 'svelte/store'

  // Source 0..1
  const src = writable(0)

  // Simple discrete color mixer
  const stepColor = (from: string, to: string) => (t: number) => (t < 0.5 ? from : to)

  const color = useTransform(src, [0, 1], ['red', 'blue'], { mixer: stepColor })
</script>

<div style="background: {$color}; width: 80px; height: 24px;" />

Compute form (auto-tracking)

Pass a compute function with no deps array. Every MotionValue whose .get() (or .current) is read inside the function is automatically tracked — motion-dom’s collectMotionValues discovers them during the initial seed call.

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

  const a = useMotionValue(2)
  const b = useMotionValue(3)

  // Recomputes whenever a or b change — no deps array required.
  const total = useTransform(() => a.get() + b.get())
</script>

<span>Total: {total.current}</span>
<script lang="ts">
  import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'

  const a = useMotionValue(2)
  const b = useMotionValue(3)

  // Recomputes whenever a or b change — no deps array required.
  const total = useTransform(() => a.get() + b.get())
</script>

<span>Total: {total.current}</span>

For mixed MotionValue + Svelte readable scenarios, sample the readable via get(readable) inside the compute (or $store syntax). Readables don’t participate in collectMotionValues, so their values are sampled when an adjacent motion value triggers a recompute.

Single-MV / multi-MV transformer forms

For straightforward 1→1 or N→1 transforms, the transformer-style overloads are terser than the compute form:

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

  // Single MV → single output
  const x = useMotionValue(10)
  const doubled = useTransform(x, (latest) => latest * 2)

  // Multi MV → single output
  const a = useMotionValue(2)
  const b = useMotionValue(3)
  const product = useTransform([a, b], ([latestA, latestB]) => latestA * latestB)
</script>
<script lang="ts">
  import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'

  // Single MV → single output
  const x = useMotionValue(10)
  const doubled = useTransform(x, (latest) => latest * 2)

  // Multi MV → single output
  const a = useMotionValue(2)
  const b = useMotionValue(3)
  const product = useTransform([a, b], ([latestA, latestB]) => latestA * latestB)
</script>

Multi-output mapping form

Map a single source to many output ranges in one call. Returns an object of motion values keyed by your map:

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

  const x = useMotionValue(0)
  const { opacity, scale } = useTransform(x, [0, 100], {
    opacity: [0, 1],
    scale: [0.5, 1]
  })
</script>

<div style="opacity: {opacity.current}; transform: scale({scale.current})">…</div>
<script lang="ts">
  import { useMotionValue, useTransform } from '@humanspeak/svelte-motion'

  const x = useMotionValue(0)
  const { opacity, scale } = useTransform(x, [0, 100], {
    opacity: [0, 1],
    scale: [0.5, 1]
  })
</script>

<div style="opacity: {opacity.current}; transform: scale({scale.current})">…</div>

How it works

  • Mapping form picks the active input segment and interpolates between its corresponding outputs.
  • clamp (default true) limits the input to current segment bounds; set false to allow extrapolation.
  • ease shapes the 0..1 progress before mixing.
  • If outputs are numeric, a linear mixer is used; otherwise provide a custom mixer.
  • Descending input ranges are supported. Equal segment endpoints produce zero progress for that segment.

Scroll acceleration

When the source is a useScroll progress value and the browser supports native scroll timelines, the mapped value runs on the compositor thread instead of updating in JavaScript. This applies to the first hop only, with clamp left on, and input stops that ascend within 0-1. Chained transforms, clamp: false, function transformers, and other ranges fall back to JavaScript. The value must be bound to opacity, filter, clipPath, transform, or backgroundColor.

<script>
  import { motion, useScroll, useTransform } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const opacity = useTransform(scrollYProgress, [0.25, 0.5], [0.2, 1])
</script>

<motion.div style={{ opacity }} />
<script>
  import { motion, useScroll, useTransform } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const opacity = useTransform(scrollYProgress, [0.25, 0.5], [0.2, 1])
</script>

<motion.div style={{ opacity }} />

API Reference

Signatures

// Mapping form
useTransform(source, input, output, options?)
  source: MotionValue<number> | Readable<number>   // numeric source
  input:  number[]                                  // input stops
  output: T[]                                       // output stops (same length as input)
  options.clamp:  boolean                           // clamp to active segment (default true)
  options.ease:   Function | Function[]             // easing per segment
  options.mixer:  (from, to) => (t) => value        // custom mixer
  Returns: AugmentedMotionValue<T>

// Single-MV transformer form
useTransform(mv, (latest) => out)
  mv:          MotionValue<I>                       // source motion value
  transformer: (latest: I) => O                     // map latest into output
  Returns: AugmentedMotionValue<O>

// Multi-MV transformer form
useTransform([mv1, mv2, …], ([a, b, …]) => out)
  sources:     Array<MotionValue>                   // source motion values
  transformer: (latest: I[]) => O                   // combine latest values
  Returns: AugmentedMotionValue<O>

// Multi-output mapping form
useTransform(source, input, outputMap, options?)
  source:    MotionValue<number> | Readable<number>
  input:     number[]
  outputMap: { [key]: T[] }                         // one output range per key
  Returns: { [key]: AugmentedMotionValue<T> }

// Compute form (auto-tracking — no deps array)
useTransform(() => compute)
  compute: () => T                                  // reads .get() on any MotionValues
  Returns: AugmentedMotionValue<T>
  // Inside compute, call mv.get() (or read mv.current) on each MotionValue
  // you want tracked. motion-dom's collectMotionValues discovers them
  // automatically during the seed call — no explicit deps array.
// Mapping form
useTransform(source, input, output, options?)
  source: MotionValue<number> | Readable<number>   // numeric source
  input:  number[]                                  // input stops
  output: T[]                                       // output stops (same length as input)
  options.clamp:  boolean                           // clamp to active segment (default true)
  options.ease:   Function | Function[]             // easing per segment
  options.mixer:  (from, to) => (t) => value        // custom mixer
  Returns: AugmentedMotionValue<T>

// Single-MV transformer form
useTransform(mv, (latest) => out)
  mv:          MotionValue<I>                       // source motion value
  transformer: (latest: I) => O                     // map latest into output
  Returns: AugmentedMotionValue<O>

// Multi-MV transformer form
useTransform([mv1, mv2, …], ([a, b, …]) => out)
  sources:     Array<MotionValue>                   // source motion values
  transformer: (latest: I[]) => O                   // combine latest values
  Returns: AugmentedMotionValue<O>

// Multi-output mapping form
useTransform(source, input, outputMap, options?)
  source:    MotionValue<number> | Readable<number>
  input:     number[]
  outputMap: { [key]: T[] }                         // one output range per key
  Returns: { [key]: AugmentedMotionValue<T> }

// Compute form (auto-tracking — no deps array)
useTransform(() => compute)
  compute: () => T                                  // reads .get() on any MotionValues
  Returns: AugmentedMotionValue<T>
  // Inside compute, call mv.get() (or read mv.current) on each MotionValue
  // you want tracked. motion-dom's collectMotionValues discovers them
  // automatically during the seed call — no explicit deps array.

Parameters

  • source MotionValue<number> | Readable<number>: Numeric source (mapping form).
  • input number[]: Input stops (length must match output).
  • output T[]: Output stops (same length as input).
  • outputMap { [key: string]: T[] }: Object of output ranges, one per key. Returns an object of motion values with the same keys.
  • options.clamp boolean (default true): Clamp to active segment.
  • options.ease ((t: number) => number) | Array<...>: Easing per segment or single easing.
  • options.mixer (from, to) => (t) => any: Custom mixer for non-numeric outputs.
  • transformer (latest) => O / ([latest, …]) => O: Transform / combine function (single-MV / multi-MV forms).
  • compute () => T: Compute function (compute form). MotionValues read via .get() inside are auto-tracked.

Returns

An AugmentedMotionValue<T> — a real motion-dom MotionValue (so it composes with useTransform, useSpring, animate(), etc.) plus:

  • .current — Svelte 5 reactive getter for templates / $derived / $effect.
  • .subscribe(run) — Svelte readable store contract (powers $transformed template syntax).
  • All other MotionValue methods from motion-dom (get, getVelocity, on, etc.).

When to use

  • Link styles directly to time or gesture progress.
  • Derive values from other stores using a declarative, reactive API.
  • Map ranges with easing and clamp behavior without manual math.
  • Interpolate non-numeric outputs via a custom mixer.

See also


Based on Motion’s useTransform API.