usePresence

usePresence and useIsPresent let a component branch on whether <AnimatePresence> is keeping it alive for an exit phase. The wrapper (<PresenceChild>) holds the child rendered while isPresent is false, and the child runs its own exit animation — CSS transition, canvas effect, GSAP, anything — and calls safeToRemove() when finished.

This is the path to take when the built-in motion.* exit prop isn’t enough — for example, animating a third-party component, fading text via CSS classes, or coordinating an exit with non-DOM work.

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

    let visible = $state(true)
</script>

<button onclick={() => (visible = !visible)}>Toggle</button>

<AnimatePresence>
    <PresenceChild present={visible}>
        <Card />
    </PresenceChild>
</AnimatePresence>
<script lang="ts">
    import { AnimatePresence, PresenceChild, usePresence } from '@humanspeak/svelte-motion'

    let visible = $state(true)
</script>

<button onclick={() => (visible = !visible)}>Toggle</button>

<AnimatePresence>
    <PresenceChild present={visible}>
        <Card />
    </PresenceChild>
</AnimatePresence>

Inside <Card>, call usePresence() once in the component’s <script>, then read presence in $effect or the template. Destructuring it at the top level of <script> reads it only once.

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

    const presence = usePresence()
    const isPresent = $derived(presence[0])
    let node: HTMLElement | undefined = $state()

    $effect(() => {
        const [present, safeToRemove] = presence
        if (present || !node || !safeToRemove) return
        const el = node
        const onEnd = (e: TransitionEvent) => {
            if (e.target !== el) return
            safeToRemove()
        }
        el.addEventListener('transitionend', onEnd, { once: true })
        return () => el.removeEventListener('transitionend', onEnd)
    })
</script>

<div bind:this={node} class="card" class:exiting={!isPresent}>…</div>

<style>
    .card { transition: opacity 300ms, transform 300ms; }
    .card.exiting { opacity: 0; transform: translateY(-12px); }
</style>
<script lang="ts">
    import { usePresence } from '@humanspeak/svelte-motion'

    const presence = usePresence()
    const isPresent = $derived(presence[0])
    let node: HTMLElement | undefined = $state()

    $effect(() => {
        const [present, safeToRemove] = presence
        if (present || !node || !safeToRemove) return
        const el = node
        const onEnd = (e: TransitionEvent) => {
            if (e.target !== el) return
            safeToRemove()
        }
        el.addEventListener('transitionend', onEnd, { once: true })
        return () => el.removeEventListener('transitionend', onEnd)
    })
</script>

<div bind:this={node} class="card" class:exiting={!isPresent}>…</div>

<style>
    .card { transition: opacity 300ms, transform 300ms; }
    .card.exiting { opacity: 0; transform: translateY(-12px); }
</style>
mode · live running open
// use-presence exits: 00
exit: css transition → safeToRemove()

API divergence from React

In framer-motion, usePresence works directly inside <AnimatePresence> — React’s render tree gives the library control over when children unmount. Svelte’s {#if} teardown is synchronous from the user’s side and not interceptable, so you opt-in via the <PresenceChild> wrapper. Bind present to the same condition that would normally gate the children:

<!-- React (framer-motion) -->
<AnimatePresence>
    {visible && <Card />}
</AnimatePresence>

<!-- Svelte equivalent -->
<AnimatePresence>
    <PresenceChild present={visible}>
        <Card />
    </PresenceChild>
</AnimatePresence>
<!-- React (framer-motion) -->
<AnimatePresence>
    {visible && <Card />}
</AnimatePresence>

<!-- Svelte equivalent -->
<AnimatePresence>
    <PresenceChild present={visible}>
        <Card />
    </PresenceChild>
</AnimatePresence>

useIsPresent

Returns just the isPresent boolean. Useful when you only need to render different content during exit, no safeToRemove needed:

<script lang="ts">
    import { useIsPresent } from '@humanspeak/svelte-motion'
    const isPresent = $derived(useIsPresent())
</script>

<div data-state={isPresent ? 'live' : 'exiting'}>…</div>
<script lang="ts">
    import { useIsPresent } from '@humanspeak/svelte-motion'
    const isPresent = $derived(useIsPresent())
</script>

<div data-state={isPresent ? 'live' : 'exiting'}>…</div>

When called outside any PresenceChild, useIsPresent() returns true and usePresence() returns [true, null]. useIsPresent() never registers with the wrapper, so it never holds an exit.

How safeToRemove behaves

  • Idempotent. Calling it twice is a no-op after the first.
  • Versioned. Re-entering (present flipping back to true) before safeToRemove fires cancels the exit; the previously-handed-out callback becomes a no-op so a stale transitionend handler can’t tear down a now-present component.
  • Per participant. Each usePresence() consumer is one participant of its wrapper, and its safeToRemove completes only that participant. The wrapper (or owned child snippet) leaves when every participant is done.

Mixing with motion.* exit

Inside <PresenceChild>, independent motion.* descendants run their exit animations on the real nodes. The wrapper automatically waits until all registered motion exits finish, then removes the whole subtree and reports one completion to <AnimatePresence>. The same coordination applies to an owned child() snippet containing a plain wrapper with multiple motion descendants.

As in Motion, every usePresence() consumer is a participant too. Its safeToRemove() completes only that consumer, so it never cuts a slower motion exit short: the wrapper leaves when every participant (motion exits, layout measurement, other usePresence() consumers) is done. A wrapper with nothing registered (plain elements, or an m without LazyMotion features) leaves as soon as its exit starts.

Re-entering cancels the current automatic exit cycle. Stale animation completions cannot finish a newer exit. Removing an exiting motion descendant also removes its pending participation, so it cannot leave the wrapper stuck.

See the owned group example for fast and slow exits that retain their common wrapper until both finish.

Migrating from $derived(usePresence())

Earlier versions asked you to re-call usePresence() inside $derived. That pattern still works but is deprecated: a reactive call can’t register with the wrapper, so the wrapper keeps the old rules. It doesn’t leave on its own when nothing else is registered, and that call’s safeToRemove releases the whole wrapper, motion descendants included. Development builds warn once per wrapper.

<!-- Before (deprecated) -->
<script lang="ts">
    const presence = $derived(usePresence())
</script>

<!-- After -->
<script lang="ts">
    const presence = usePresence()
</script>
<!-- Before (deprecated) -->
<script lang="ts">
    const presence = $derived(usePresence())
</script>

<!-- After -->
<script lang="ts">
    const presence = usePresence()
</script>

The rest of the component ($derived(presence[0]), destructuring inside $effect) stays the same.

Known limitations

  • Nested <AnimatePresence> inside a held <PresenceChild>: while the wrapper is holding, descendants don’t see exit signals because the Svelte tree is still mounted. Once the wrapper leaves, normal unmount fires, and any nested motion children’s exit runs at that point.

API Reference

<PresenceChild> props

PropTypeDefaultDescription
presentbooleantrueWhen this flips true → false, the wrapper holds children rendered with isPresent=false until every registered participant is done (with nothing registered, at once).
childrenSnippet—Snippet rendered while present is true or while the wrapper is holding.

useIsPresent(): boolean

Returns whether the calling component is currently present. true outside of any <PresenceChild>.

usePresence(): [true, null] | [false, () => void]

Registers the calling component with its wrapper at init and returns a reactive two-item array in the framer-motion tuple shape: [true, null] while present (or outside any <PresenceChild>); [false, () => void] once the wrapper enters its exit hold. The callback completes only this consumer.

See also


Based on Motion’s usePresence API.