useAnimationControls

useAnimationControls creates a legacy imperative controller. Pass the returned object to one or more motion.* components via animate={controls}, then call controls.start(...), controls.set(...), or controls.stop().

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

    const controls = useAnimationControls()

    const run = async () => {
        await controls.start('launch')
        await controls.start('complete')
    }
</script>

<button onclick={run}>Start</button>
<motion.div
    animate={controls}
    variants={{
        launch: { x: 100 },
        complete: { x: 0, scale: 1.1 }
    }}
/>
<script lang="ts">
    import { motion, useAnimationControls } from '@humanspeak/svelte-motion'

    const controls = useAnimationControls()

    const run = async () => {
        await controls.start('launch')
        await controls.start('complete')
    }
</script>

<button onclick={run}>Start</button>
<motion.div
    animate={controls}
    variants={{
        launch: { x: 100 },
        complete: { x: 0, scale: 1.1 }
    }}
/>
mode · live running open
// use-animation-controls status: ready
ready
run 0
one controls object, four targets runs: 00

Sequencing

controls.start(definition) returns a promise. If the controls are subscribed to three motion components, that promise resolves after all three components finish their animation.

<script lang="ts">
    const controls = useAnimationControls()

    const submit = async () => {
        await controls.start('loading', { duration: 0.4 })
        await controls.start('success', { duration: 0.3 })
    }
</script>
<script lang="ts">
    const controls = useAnimationControls()

    const submit = async () => {
        await controls.start('loading', { duration: 0.4 })
        await controls.start('success', { duration: 0.3 })
    }
</script>

start vs set vs stop

The three commands have deliberately different relationships to time, matching upstream:

  • start(definition) animates from the currently rendered value to the target — including the very first command on an element idling at a non-neutral pose (the animation departs from the idle value, never from a wiped base).
  • set(definition) applies the values instantly — no animation, by design. It is the imperative “pose” primitive: set('hidden') then start('visible') is the canonical pose-then-animate pattern. If you expected motion and saw a snap, you wanted start.
  • stop() freezes every in-flight animation at its current mid-flight value and holds it there — through later re-renders — rather than jumping to the target or reverting to a previous state.

Wildcard and relative values

Keyframe wildcards and relative strings resolve against the element’s live value the moment the animation starts:

<script lang="ts">
    const controls = useAnimationControls()

    // [null, 100]: the live value feeds the FIRST keyframe, so this animates
    // from wherever x currently is → 100. (Later `null`s hold the previous
    // keyframe instead: [0, null] plays [0, 0].)
    const slideFromHere = () => controls.start({ x: [null, 100] })

    // Relative strings offset from the live value: x moves 50px further right.
    const nudge = () => controls.start({ x: '+=50' })

    // A bare `null` holds a channel at its current value — useful to pin one
    // channel while animating another.
    const pulseInPlace = () => controls.start({ scale: [null, 1.15, 1], x: null })
</script>
<script lang="ts">
    const controls = useAnimationControls()

    // [null, 100]: the live value feeds the FIRST keyframe, so this animates
    // from wherever x currently is → 100. (Later `null`s hold the previous
    // keyframe instead: [0, null] plays [0, 0].)
    const slideFromHere = () => controls.start({ x: [null, 100] })

    // Relative strings offset from the live value: x moves 50px further right.
    const nudge = () => controls.start({ x: '+=50' })

    // A bare `null` holds a channel at its current value — useful to pin one
    // channel while animating another.
    const pulseInPlace = () => controls.start({ scale: [null, 1.15, 1], x: null })
</script>

The same grammar works on the declarative animate prop. See the keyframes example for the visual version.

Variants

Each subscribed component resolves the same variant label against its own variants map. This mirrors Motion’s VisualElement fan-out behavior:

<motion.div
    animate={controls}
    variants={{
        loading: { scale: 1.1 },
        success: { scale: 1 }
    }}
/>

<motion.span
    animate={controls}
    variants={{
        loading: { opacity: 0.5 },
        success: { opacity: 1 }
    }}
/>
<motion.div
    animate={controls}
    variants={{
        loading: { scale: 1.1 },
        success: { scale: 1 }
    }}
/>

<motion.span
    animate={controls}
    variants={{
        loading: { opacity: 0.5 },
        success: { opacity: 1 }
    }}
/>

API Reference

useAnimationControls()

Returns an AnimationControls object:

  • start(definition, transitionOverride?) — animates every subscribed component and returns Promise<unknown[]>.
  • set(definition) — synchronously sets every subscribed component to the target’s final values. Variant labels and transitionEnd values are resolved.
  • stop() — stops active subscriber animations.
  • mount() — internal lifecycle hook called automatically by useAnimationControls().
  • subscribe(subscriber) — internal component subscription hook.

start and set throw if called before the hook’s component has mounted, matching upstream Motion’s guard against render-synchronous control calls.

Alias

useAnimation is exported as an alias of useAnimationControls, matching Motion’s legacy API.

Related

  • useAnimate — scoped selector-based imperative animation
  • Variants — named animation states

Based on Motion’s useAnimationControls API.