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>:
<script lang="ts">
import { usePresence } from '@humanspeak/svelte-motion'
const presence = $derived(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 = $derived(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>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].
How safeToRemove behaves
- Idempotent. Calling it twice is a no-op after the first.
- Versioned. Re-entering (
presentflipping back totrue) beforesafeToRemovefires cancels the exit; the previously-handed-out callback becomes a no-op so a staletransitionendhandler can’t tear down a now-present component. - Manual holds. A wrapper with no motion exit participants stays mounted
until you call
safeToRemove. ReadingusePresence()does not add an independent hold when motion descendants already coordinate automatic removal.
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.
safeToRemove() remains an explicit release of the whole wrapper. Calling
it may remove motion descendants before their animations finish. Merely reading usePresence() does not register a second completion barrier. For custom exit
work that must hold independently, use a separate manual <PresenceChild> boundary rather than combining it with automatic motion exits in one wrapper.
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.
Known limitations
mode='popLayout': the wrapper holds the child in document flow during exit, sopopLayoutsemantics (sibling reflow as the exiting element leaves layout immediately) are not implemented for<PresenceChild>.mode='sync'(default) andmode='wait'work as expected — the wrapper participates in the sameinFlightExitsaccounting as the clone path.- 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 you callsafeToRemove, normal unmount fires, and any nested motion children’sexitruns at that point.
API Reference
<PresenceChild> props
| Prop | Type | Default | Description |
|---|---|---|---|
present | boolean | true | When this flips true → false, the wrapper holds children rendered with isPresent=false until all motion exits finish or safeToRemove fires. |
children | Snippet | — | 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]
Returns the framer-motion-style tuple. [true, null] while present (or
outside any <PresenceChild>); [false, () => void] once the wrapper
enters its exit hold.
See also
- AnimatePresence — the parent component.
motion.*exitprop — declarative alternative when a built-in transform/style animation is enough.
Based on Motion’s usePresence API.