Beta Status

This library is still in early beta and the API is subject to change and may get daily breaking changes. The documentaton may not be up to date with the latest features and include missing or outdated information. You can always create an issue on GitHub or even better a pull request to fix the documentation or add new features.

AnimatePresence

AnimatePresence enables exit animations for components leaving the tree. Without it, elements disappear instantly without animating.

Basic Usage

import { AnimatePresence, motion } from "motion-solid";
import { Show } from "solid-js";

function Toast() {
  const [visible, setVisible] = createSignal(true);

  return (
    <AnimatePresence>
      <Show when={visible()}>
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, x: 100 }}
          class="toast"
        >
          Notification message
        </motion.div>
      </Show>
    </AnimatePresence>
  );
}

Only motion.* elements inside AnimatePresence receive exit animations.

Modes

Control how enter and exit animations coordinate:

sync (default)

Enter and exit animations run simultaneously:

<AnimatePresence mode="sync">{/* ... */}</AnimatePresence>

wait

Entering elements wait for all exits to complete:

<AnimatePresence mode="wait">
  <Show when={tab() === "a"}>
    <motion.div exit={{ opacity: 0 }} />
  </Show>
  <Show when={tab() === "b"}>
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
  </Show>
</AnimatePresence>

Useful for tab switches or page transitions where you want clean handoff.

popLayout

Exiting elements are taken out of the document flow while animating:

<AnimatePresence mode="popLayout">
  <For each={items()}>
    {(item) => <motion.li layout exit={{ opacity: 0, scale: 0.5 }} />}
  </For>
</AnimatePresence>

This prevents layout shifts during exit animations. popLayout temporarily applies inline position, min-width, and min-height styles, then restores them after exits complete.

Props

initial

Set false to skip initial animations for elements present on first render:

// Children animate in on first render
<AnimatePresence>
  {/* ... */}
</AnimatePresence>

// Children appear immediately, only exit animations run
<AnimatePresence initial={false}>
  {/* ... */}
</AnimatePresence>

custom

Pass data to variant functions for all children:

const variants = {
  exit: (direction: number) => ({
    x: direction > 0 ? -100 : 100,
    opacity: 0,
  }),
};

const [direction, setDirection] = createSignal(1);

<AnimatePresence custom={direction()} mode="wait">
  <Show when={page() === 1}>
    <motion.div variants={variants} exit="exit" />
  </Show>
</AnimatePresence>;

onExitComplete

Callback when all exiting elements finish:

<AnimatePresence onExitComplete={() => console.log("all done")}>
  {/* ... */}
</AnimatePresence>

propagate

When true, parent removal triggers child exit animations. Default false:

// Child exits don't run when parent exits
<AnimatePresence>
  <Show when={showParent()}>
    <motion.div exit={{ opacity: 0 }}>
      <AnimatePresence>
        <motion.div exit={{ scale: 0 }} /> {/* won't run */}
      </AnimatePresence>
    </motion.div>
  </Show>
</AnimatePresence>

// Child exits run when parent exits
<AnimatePresence propagate>
  <Show when={showParent()}>
    <motion.div exit={{ opacity: 0 }}>
      <AnimatePresence>
        <motion.div exit={{ scale: 0 }} /> {/* will run */}
      </AnimatePresence>
    </motion.div>
  </Show>
</AnimatePresence>

root

Root element for popLayout positioning. Useful with shadow DOM or portals.

Hooks

usePresence

Get presence state and a safeToRemove callback for manual control:

import { usePresence } from "motion-solid";

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

  createEffect(() => {
    if (!isPresent()) {
      // Do async work before removal
      fetchData().finally(() => safeToRemove?.());
    }
  });

  return <div>{isPresent() ? "Here" : "Leaving..."}</div>;
}

isPresent is an accessor that returns:

  • true - element is present
  • false - element is exiting

useIsPresent

Just the presence accessor, without the callback:

import { useIsPresent } from "motion-solid";

const isPresent = useIsPresent();

// Use in computed values
const opacity = () => (isPresent() ? 1 : 0.5);

usePresenceData

Access the custom payload passed to AnimatePresence:

import { usePresenceData } from "motion-solid";

const custom = usePresenceData<{ id: string }>();

Solid-Specific Behavior

Solid disposes reactive effects when a component is removed. AnimatePresence keeps the DOM node in place for exit animations, but the exiting subtree is no longer reactive.

This means:

  • Signals inside exiting components won't update
  • Effects won't re-run
  • Only the exit animation plays

Plan accordingly: compute what you need before the exit starts.

Example: Slide Transitions

import { AnimatePresence, motion } from "motion-solid";
import { createSignal } from "solid-js";

const variants = {
  enter: (direction: number) => ({
    x: direction > 0 ? 300 : -300,
    opacity: 0,
  }),
  center: { x: 0, opacity: 1 },
  exit: (direction: number) => ({
    x: direction < 0 ? 300 : -300,
    opacity: 0,
  }),
};

function Slides() {
  const [page, setPage] = createSignal(0);
  const [direction, setDirection] = createSignal(1);

  const paginate = (dir: number) => {
    setDirection(dir);
    setPage((p) => p + dir);
  };

  return (
    <>
      <AnimatePresence custom={direction()} mode="wait">
        <motion.div
          key={page()}
          variants={variants}
          custom={direction()}
          initial="enter"
          animate="center"
          exit="exit"
          transition={{ duration: 0.3 }}
        >
          Page {page()}
        </motion.div>
      </AnimatePresence>
      <button onClick={() => paginate(-1)}>Prev</button>
      <button onClick={() => paginate(1)}>Next</button>
    </>
  );
}