motion-solid

AnimatePresence

AnimatePresence keeps a removed component in the DOM until its exit animation has finished. Use it for anything that leaves the page: modals, toasts, list items, slides.

With Show

In Solid, control flow adds and removes components. motion-solid exports its own Show and For, with the same props as Solid's. Inside an AnimatePresence, they keep a removed child alive until its exit is done:

import { AnimatePresence, motion, Show } from "@motion-s/solid"

function Example(props: { isVisible: boolean }) {
    return (
        <AnimatePresence>
            <Show when={props.isVisible}>
                <motion.div
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    exit={{ opacity: 0 }}
                />
            </Show>
        </AnimatePresence>
    )
}

exit takes the same values as animate: a target, a variant label, or keyframes. It can have its own transition, which is used instead of the component's transition:

<motion.div exit={{ opacity: 0, transition: { duration: 0.2 } }} />

If a child has several motion components with exit, it's removed once all of them have finished. Variants propagate through the tree as usual, so a parent's exit label also runs the exit of its variant children.

With For

Items removed from a For exit where they were. Items that are added or come back enter with their initial animation. Give the items layout and the ones that stay move to their new position:

  • Item 1
  • Item 2
  • Item 3

An item that's added back while it's still exiting is the same component instance: its exit reverses and its state is kept.

Swapping one child for another

React's AnimatePresence tells children apart by their key, and changing a key swaps one child for another. In Solid, use <Show when={value()} keyed>: when value() changes, the old child exits and a new one is created for the new value.

<AnimatePresence>
    <Show when={photos[index()]} keyed>
        {(photo) => <motion.img src={photo.src} exit={{ opacity: 0 }} />}
    </Show>
</AnimatePresence>

when must be truthy for the child to render. If the value can be 0 or an empty string, pass an object instead (here, the photo rather than its index).

Reading data while exiting

An exiting child keeps running: its reactive content still updates. If it reads data that's gone, for example record()!.name after the record was removed, that read throws. Read the value through the callback form of Show: its argument keeps the last value while the child exits.

<AnimatePresence>
    <Show when={selected()}>
        {(record) => <motion.p exit={{ opacity: 0 }}>{record().name}</motion.p>}
    </Show>
</AnimatePresence>

The props of an exiting motion component are frozen: they keep the values they had when the exit started, so a prop change can't restart its animate target in the middle of an exit. To give an exiting component new data, use the custom prop of AnimatePresence (see below).

Props

PropTypeDefaultDescription
initialbooleantrueWith false, children present on the first render start at their animate values, without their initial animation.
mode"sync" | "wait" | "popLayout""sync"How entering and exiting children are sequenced. See Modes.
customanyData for the dynamic variants of exiting children. See Dynamic exit animations.
onExitComplete() => voidCalled once every exiting child has finished and been removed.
propagatebooleanfalseRun the exits of these children when a parent AnimatePresence removes the component around them. Read once.
presenceAffectsLayoutbooleantrueWith false, children entering or exiting don't make their siblings with layout animate.
anchorX"left" | "right""left"With mode="popLayout", the side the exiting child stays attached to horizontally.
anchorY"top" | "bottom""top"With mode="popLayout", the side the exiting child stays attached to vertically.
rootHTMLElement | ShadowRootdocument.headWith mode="popLayout", where the style that positions the exiting child is added. Set it inside a shadow DOM.

Modes

sync

The default. Children enter and exit at the same time, as soon as they're added or removed.

wait

The entering child is created once the exiting child has finished. Use it to swap one element for another in the same spot, like the steps of a wizard or the label of a button:

Plan

mode="wait" expects one child at a time. With more than one, it warns in development.

popLayout

The exiting child is taken out of the page layout as soon as it starts exiting: it's positioned absolutely where it was, and its siblings can take its place right away. Combine it with layout on the siblings so they move into the free space while the child fades out:

If the exiting element was placed against the right or bottom edge of its parent, set anchorX or anchorY so it stays there while it exits.

Dynamic exit animations

A variant can be a function of the custom value. When a component exits, its props are frozen, so it can't receive a new custom value through its own props. Pass it to AnimatePresence instead: exiting children resolve their variants with the latest custom of their AnimatePresence.

A slideshow is the usual case: the old slide leaves to the left or to the right depending on the button pressed after it was rendered.

One

Entering children get their custom value from their own custom prop, so pass it to both.

Nested presence

By default, only the children of the AnimatePresence that removes them exit. When a parent AnimatePresence removes a component that contains another AnimatePresence, the inner children are removed with it, without their exit. Set propagate on the inner one to run their exits too:

<AnimatePresence>
    <Show when={isOpen()}>
        <motion.div exit={{ opacity: 0 }}>
            <AnimatePresence propagate>
                <For each={notifications()}>
                    {(notification) => (
                        <motion.p exit={{ x: -20, opacity: 0 }}>
                            {notification.text}
                        </motion.p>
                    )}
                </For>
            </AnimatePresence>
        </motion.div>
    </Show>
</AnimatePresence>

Which children take part

Like in React, an AnimatePresence only governs its direct children. motion-solid's Show and For take part when the AnimatePresence reaches them while resolving its children: directly, or through components, fragments, conditionals and Solid's own control flow.

They don't take part when they're inside an element, a context provider (including LayoutGroup), Loading, Errored, a Portal, or inside what another presence-aware Show or For renders. There, they behave like Solid's own Show and For. Put another AnimatePresence around them if their children need to exit.

// The Show takes part: it's rendered by a component, without an element in between.
function Toast(props: { message?: string }) {
    return (
        <Show when={props.message}>
            {(message) => (
                <motion.div exit={{ opacity: 0 }}>{message()}</motion.div>
            )}
        </Show>
    )
}

function App(props: { message?: string }) {
    return (
        <AnimatePresence>
            <Toast message={props.message} />
        </AnimatePresence>
    )
}

Outside an AnimatePresence, motion-solid's Show and For are Solid's own. The only addition: For makes layout items animate when it reorders them (see Layout animations).

Solid's own Show and For

Solid's Show and For dispose a removed child right away. Directly inside an AnimatePresence, their motion children still play their exit, but with limits:

  • a child that comes back is a new instance, while the old one finishes exiting,
  • reactive content, cleanups and portals inside an exiting child don't keep running,
  • in mode="wait", entering children only wait for the exits of the same Show or For.

Use motion-solid's Show and For inside AnimatePresence. They don't have these limits.

Hooks

Components rendered inside an AnimatePresence can read their presence with these hooks. Outside an AnimatePresence, a component is always present.

usePresence

usePresence() returns [isPresent, safeToRemove]. isPresent is an accessor, false once the component has been removed. The component then stays in the DOM until safeToRemove() is called. Use it to run an exit that isn't a motion animation:

import { usePresence } from "@motion-s/solid"
import { createEffect } from "solid-js"

function Notice() {
    const [isPresent, safeToRemove] = usePresence()

    createEffect(isPresent, (present) => {
        if (!present) setTimeout(() => safeToRemove?.(), 300)
    })

    return (
        <div class={isPresent() ? "notice" : "notice notice-leaving"}>
            Saved
        </div>
    )
}

safeToRemove is null outside an AnimatePresence. Calling it while the component is present does nothing. The exit only waits for it when the component is rendered by motion-solid's Show or For.

useIsPresent

useIsPresent() returns the isPresent accessor alone. The exit doesn't wait for it.

const isPresent = useIsPresent()

<button disabled={!isPresent()}>Undo</button>

usePresenceData

usePresenceData() returns an accessor of the custom value of the closest AnimatePresence. It updates while the component exits, so the slideshow above could read the direction with it:

const direction = usePresenceData()

Exits with nothing to animate

A child without exit, without a running layout animation and without usePresence is removed in the same update that removes it. onExitComplete still fires, after that update, together with the other exits of the AnimatePresence. React renders such a child one more time before removing it.