<!-- Source: https://motion.svelte.page/docs/custom-effects -->

# Custom effects

> Drive any object with animate() — createEffect, animate.addEffect and MotionValueState.

**Source:** [https://motion.svelte.page/docs/custom-effects](https://motion.svelte.page/docs/custom-effects)

---

Custom effects teach `animate()` how to drive non-DOM subjects. Claim a kind of plain JavaScript object, describe how its properties are read and written, and it gets Motion's springs, keyframes, timing, and playback controls.

```ts
import { animate } from '@humanspeak/svelte-motion'
import { dialEffect } from './dialEffect'

animate.addEffect(dialEffect)
animate(dial, { angle: 270, radius: 80 }, { type: 'spring' })
```

> Live example: [/examples/custom-effects](https://motion.svelte.page/examples/custom-effects)

## Writing an effect

`createEffect(addValue, { test, read, step })` creates an effect for one subject type. The `addValue` callback binds each property to the `MotionValueState` shared by that subject:

```ts
import { createEffect, frame } from '@humanspeak/svelte-motion'

type Dial = { angle: number; radius: number }

const dialEffect = createEffect<Dial>(
  (dial, state, key, value) =>
    state.set(key, value, () => {
      (dial as Record<string, number>)[key] = value.get() as number
    }),
  {
    test: (subject): subject is Dial =>
      typeof subject === 'object' && subject !== null && 'angle' in subject && 'radius' in subject,
    read: (dial, key) => (dial as Record<string, number>)[key],
    step: frame.preRender
  }
)
```

The `state.set(key, value, render, computed?)` call binds the motion value and schedules its renderer when the value changes. Read `value.get()` inside the renderer to write the current value. The optional `computed` argument shares the renderer of another bound motion value.

| Option | Description |
|---|---|
| `test` | Claims matching subjects passed to `animate()`. |
| `read` | Seeds the first keyframe from the subject. Return `undefined` to require explicit `[from, to]` keyframes. |
| `step` | Selects the frameloop phase. It defaults to `frame.render`; use `frame.preRender` when your render loop runs in `frame.render`. |

## Registering with animate

Register the effect once, at module scope, alongside the effect itself:

```ts
import { animate, createEffect } from '@humanspeak/svelte-motion'

export const dialEffect = createEffect<Dial>(/* … */)

animate.addEffect(dialEffect)
```

The registry is process-global and dedupes by effect identity instead of counting registrations, so a single `removeEffect` unregisters an effect for the whole application no matter how many places registered it. Registering from a component's lifecycle is therefore unsafe: with two components sharing one effect, the first teardown unregisters it for the second, and `animate()` then falls back to the plain-object animator silently rather than failing. Keep `addEffect` at module scope, and reach for `removeEffect` only when tearing down an integration you own outright.

When multiple effects match, the most recently added effect wins. DOM elements are never claimed by registered custom effects, and plain objects that no effect claims fall back to Motion's object animator.

## Binding values manually

Effects can also bind values without going through `animate()`. Calling `effect(subject, { x })` returns an unbind function, while `effect.get(subject, 'x')` returns the bound value:

```ts
import { motionValue } from '@humanspeak/svelte-motion'

const angle = motionValue(0)
const unbind = dialEffect(dial, { angle })

dialEffect.get(dial, 'angle')?.on('change', (latest) => {
  console.log(latest)
})

unbind()
angle.destroy()
```

`propEffect` provides the same `.get(subject, key)` lookup for manually bound object properties.

Custom effects also expose upstream's `.flush(subject)` to write pending values immediately, and `.state(subject)` to access the subject's bound `MotionValueState`. Use `flush` before measuring a subject that needs its latest values applied.

## Three.js and vgpu

Motion ships adapters for Three.js and vgpu. Import them from this package's subpaths — `three`/`vgpu` stay your own dependencies:

```ts
import { animate } from '@humanspeak/svelte-motion'
import { threeEffect } from '@humanspeak/svelte-motion/three'

animate.addEffect(threeEffect)
animate(mesh, { rotateY: 360, scaleX: 1.4 }, { type: 'spring' })
animate(material.uniforms, { progress: 1 })
```

## Related

- [Vanilla motion values](/docs/vanilla-values) — component-free values and element effects
- [Three.js effect](/docs/three-effect) — animate meshes, materials, and shader uniforms
- [Custom effects example](/examples/custom-effects) — a canvas dial with complete source

---

Based on Motion 13.2's [animate.addEffect](https://motion.dev/docs/animate) and `createEffect`.
