<!-- Source: https://motion.svelte.page/examples/animate-presence -->

# AnimatePresence

> Animate components when they are added to or removed from the DOM using Svelte Motion. Smooth enter and exit transitions made easy.

**Source:** [https://motion.svelte.page/examples/animate-presence](https://motion.svelte.page/examples/animate-presence)

**Markdown mirror:** [https://motion.svelte.page/examples/animate-presence.md](https://motion.svelte.page/examples/animate-presence.md)

---

This mirror preserves the prose, implementation notes, and runnable Svelte source behind the live example page.

## FIG-001: enter + exit with AnimatePresence.

The existing conditional API snapshots a removed `motion.*` element and runs its `exit` prop on a visual clone.

**Metadata:** tag: `ANIMATE-PRESENCE` | pattern: `enter + exit`

### Notes

- The unit card has `initial`, `animate`, and `exit` props. On mount, the real node tweens from `initial → animate`. On removal, Svelte destroys that node and AnimatePresence runs `animate → exit` on its captured clone.
- Without `AnimatePresence`, a Svelte `&#123;#if&#125;` would tear the node down immediately and the `exit` animation would never get a chance to run. The conditional AnimatePresence form captures a clone before teardown and animates that detached snapshot.
- The `transition` applies to all three phases — same spring ( stiffness: 300, damping: 25 ) on the way in and on the way out, so the motion feels symmetric instead of snappy-in / linear-out.

### Source

#### Default.svelte

Source file: [src/lib/examples/animate-presence/demos/Default.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/animate-presence/demos/Default.svelte)

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

    // A `motion.*` element inside `<AnimatePresence>` with an `exit` prop runs
    // that animation when the element is removed from the DOM. Toggle the
    // unit — the card scales + fades in on mount, then runs its `exit`
    // (scale + fade out) when it leaves, all on the same spring.
    let isVisible = $state(true)
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <MotionConfig transition={{ duration: 0.6 }}>
        <div class="strip">
            <div class="strip-head">
                <span class="micro">// presence</span>
                <span class="micro state">
                    state: {isVisible ? 'mounted' : 'unmounted'}
                </span>
            </div>

            <!-- Fixed-height stage so the footer never moves as the unit enters/exits. -->
            <div class="stage">
                <AnimatePresence>
                    {#if isVisible}
                        <motion.div
                            key="unit"
                            initial={{ opacity: 0, scale: 0 }}
                            animate={{ opacity: 1, scale: 1 }}
                            exit={{ opacity: 0, scale: 0 }}
                            transition={{ type: 'spring', stiffness: 300, damping: 25 }}
                            style={styleString(() => ({
                                transformOrigin: 'center'
                            }))}
                        >
                            <article class="unit">
                                <header class="unit-head">
                                    <motion.span
                                        animate={{ opacity: [1, 0.35, 1] }}
                                        transition={{
                                            duration: 1.6,
                                            repeat: Infinity,
                                            ease: 'easeInOut'
                                        }}
                                        style={styleString(() => ({
                                            flex: 'none',
                                            width: 8,
                                            height: 8,
                                            borderRadius: '50%',
                                            backgroundColor: 'var(--brut-accent, #247768)'
                                        }))}
                                    ></motion.span>
                                    <span class="unit-name">unit.01</span>
                                </header>
                                <dl class="unit-meta">
                                    <div>
                                        <dt>enter</dt>
                                        <dd>scale 0 → 1</dd>
                                    </div>
                                    <div>
                                        <dt>exit</dt>
                                        <dd>scale 1 → 0</dd>
                                    </div>
                                </dl>
                            </article>
                        </motion.div>
                    {/if}
                </AnimatePresence>
            </div>

            <div class="strip-foot">
                <motion.button
                    onclick={() => (isVisible = !isVisible)}
                    whileHover={{ scale: 1.04 }}
                    whileTap={{ scale: 0.96 }}
                    transition={{ type: 'spring', stiffness: 500, damping: 30 }}
                    style={styleString(() => ({
                        fontFamily: 'var(--brut-mono, monospace)',
                        fontSize: '0.6875rem',
                        textTransform: 'uppercase',
                        letterSpacing: '0.08em',
                        border: '1px solid var(--brut-accent, #247768)',
                        backgroundColor: 'var(--brut-accent-soft, rgba(36, 119, 104, 0.1))',
                        color: 'var(--brut-accent, #247768)',
                        padding: '0.5rem 0.875rem',
                        cursor: 'pointer'
                    }))}
                >
                    {isVisible ? '– unmount unit' : '+ mount unit'}
                </motion.button>
                <span class="micro">spring: 300 / 25</span>
            </div>
        </div>
    </MotionConfig>
</div>

<style>
    .dk-demo-shell {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 1.5rem;
        min-height: 360px;
    }

    .strip {
        width: 100%;
        max-width: 420px;
        display: flex;
        flex-direction: column;
        gap: 0.75rem;
    }

    .micro {
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--brut-ink-3, #9a9a9a);
    }

    .state {
        color: var(--brut-accent, #247768);
    }

    .strip-head,
    .strip-foot {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        border-bottom: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-bottom: 0.5rem;
    }

    .strip-foot {
        border-bottom: none;
        border-top: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-top: 0.75rem;
        padding-bottom: 0;
    }

    .stage {
        height: 9rem;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .unit {
        width: 13rem;
        display: flex;
        flex-direction: column;
        gap: 0.75rem;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
        padding: 0.875rem;
        box-shadow: 6px 6px 0 var(--brut-rule, #d6dedb);
    }

    .unit-head {
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }

    .unit-name {
        font-family: var(--brut-mono, monospace);
        font-size: 0.75rem;
        font-weight: 700;
        color: var(--brut-ink, #0a0a0a);
    }

    .unit-meta {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
        margin: 0;
    }

    .unit-meta div {
        display: flex;
        justify-content: space-between;
        gap: 0.5rem;
    }

    .unit-meta dt {
        font-family: var(--brut-mono, monospace);
        font-size: 0.625rem;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: var(--brut-ink-3, #9a9a9a);
    }

    .unit-meta dd {
        margin: 0;
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
        color: var(--brut-ink-2, #525252);
    }
</style>
```

## FIG-002: exit the real node.

`present` plus a named `child` snippet lets AnimatePresence retain the original DOM subtree until its nested `motion.*` exit completes.

**Metadata:** tag: `OWNED-CHILD` | mechanism: `real node`

### Notes

- The element that exits is the original DOM node, so live state and its VisualElement remain available for the whole animation.
- Use the owned API when clone fidelity is not sufficient—for example, focus-sensitive content, reversible exits, or a canvas-driven integration.
- Toggling `present` back to true cancels an in-flight exit and keeps stale completion callbacks from removing the re-entered node.

### Source

#### OwnedChildMotion.svelte

Source file: [src/lib/examples/animate-presence/demos/OwnedChildMotion.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/animate-presence/demos/OwnedChildMotion.svelte)

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

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

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="demo">
        <div class="stage">
            <AnimatePresence present={visible} onExitComplete={() => (completed += 1)}>
                {#snippet child()}
                    <motion.div
                        initial={{ opacity: 0, y: 18 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0, y: -18, scale: 0.92 }}
                        transition={{ duration: 0.45, ease: 'easeInOut' }}
                        style={styleString(() => ({
                            width: 210,
                            border: '1px solid var(--brut-accent, #247768)',
                            backgroundColor: 'var(--brut-bg-2, #eef4f1)',
                            color: 'var(--brut-ink, #0a0a0a)',
                            padding: 18,
                            boxShadow: '6px 6px 0 var(--brut-rule, #d6dedb)'
                        }))}
                    >
                        <p class="label">real DOM node</p>
                        <strong>AnimatePresence owns this child</strong>
                        <p class="detail">Exit completes before this node is removed.</p>
                    </motion.div>
                {/snippet}
            </AnimatePresence>
        </div>

        <div class="controls">
            <motion.button
                onclick={() => (visible = !visible)}
                whileHover={{ scale: 1.04 }}
                whileTap={{ scale: 0.96 }}
                style={styleString(() => ({
                    border: '1px solid var(--brut-accent, #247768)',
                    backgroundColor: 'var(--brut-accent-soft, rgba(36, 119, 104, 0.1))',
                    color: 'var(--brut-accent, #247768)',
                    padding: '0.5rem 0.75rem',
                    cursor: 'pointer'
                }))}
            >
                {visible ? 'run real-node exit' : 'mount again'}
            </motion.button>
            <span>completed: {completed}</span>
        </div>
    </div>
</div>

<style>
    .dk-demo-shell {
        width: 100%;
        min-height: 340px;
        display: grid;
        place-items: center;
        box-sizing: border-box;
        padding: 1.5rem;
    }

    .demo {
        width: 520px;
        max-width: 100%;
    }

    .stage {
        height: 190px;
        display: grid;
        place-items: center;
        border: 1px dashed var(--brut-rule-2, #bbc4c0);
    }

    .label,
    .detail {
        margin: 0;
        font-family: var(--brut-mono, monospace);
    }

    .label {
        color: var(--brut-accent, #247768);
        font-size: 0.625rem;
        letter-spacing: 0.1em;
        text-transform: uppercase;
    }

    strong {
        display: block;
        margin-top: 0.5rem;
    }

    .detail {
        margin-top: 0.35rem;
        color: var(--brut-ink-2, #525252);
        font-size: 0.6875rem;
    }

    .controls {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        margin-top: 0.75rem;
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
    }
</style>
```

## FIG-003: clone ghost vs. live node.

Toggle each lane twice quickly. The conditional form mounts a new node beside an exiting ghost; `present={visible}` reverses the original node in place.

**Metadata:** tag: `MECHANISM-COMPARISON` | decisive test: `rapid re-entry`

### Notes

- Click a lane’s button twice before its exit finishes. This makes node identity visible instead of comparing two similar-looking one-way fades.
- The clone lane has two identities during rapid re-entry: a new live node and the old detached ghost. The owned lane keeps one identity and reverses it.
- Edit each input before toggling. Only the owned lane can preserve that live browser state while an exit is reversed.

### Source

#### CloneVsOwned.svelte

Source file: [src/lib/examples/animate-presence/demos/CloneVsOwned.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/animate-presence/demos/CloneVsOwned.svelte)

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

    let cloneVisible = $state(true)
    let ownedVisible = $state(true)

    const exit = { opacity: 0, y: -22, scale: 0.92 }
    const transition = { duration: 0.7, ease: 'linear' as const }
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="comparison">
        <div class="prompt">
            <strong>Try the decisive test</strong>
            <span>Click each lane’s button twice quickly.</span>
        </div>

        <div class="lanes">
            <section class="lane clone">
                <header>
                    <span>Existing API</span>
                    <h3>Clone fallback</h3>
                </header>
                <div class="stage">
                    <AnimatePresence>
                        {#if cloneVisible}
                            <motion.div
                                key="clone-card"
                                class="card"
                                initial={{ opacity: 0, y: 14 }}
                                animate={{ opacity: 1, y: 0 }}
                                {exit}
                                {transition}
                            >
                                <strong>Detached ghost</strong>
                                <input aria-label="Clone path state" value="edit me" />
                            </motion.div>
                        {/if}
                    </AnimatePresence>
                </div>
                <button onclick={() => (cloneVisible = !cloneVisible)}>
                    {cloneVisible ? 'exit clone path' : 'mount new node'}
                </button>
                <p>Rapid re-entry creates a new node while the old clone keeps exiting.</p>
            </section>

            <section class="lane owned">
                <header>
                    <span>Owned API</span>
                    <h3>Real node</h3>
                </header>
                <div class="stage">
                    <AnimatePresence present={ownedVisible}>
                        {#snippet child()}
                            <motion.div
                                class="card"
                                initial={{ opacity: 0, y: 14 }}
                                animate={{ opacity: 1, y: 0 }}
                                {exit}
                                {transition}
                            >
                                <strong>Original instance</strong>
                                <input aria-label="Owned path state" value="edit me" />
                            </motion.div>
                        {/snippet}
                    </AnimatePresence>
                </div>
                <button onclick={() => (ownedVisible = !ownedVisible)}>
                    {ownedVisible ? 'exit real node' : 'reverse / mount'}
                </button>
                <p>Rapid re-entry reverses the same node and preserves its live state.</p>
            </section>
        </div>
    </div>
</div>

<style>
    .dk-demo-shell {
        min-height: 430px;
        display: grid;
        place-items: center;
        padding: 1.5rem;
    }

    .comparison {
        width: min(100%, 760px);
    }

    .prompt {
        display: flex;
        align-items: center;
        gap: 0.75rem;
        border-left: 3px solid #d6a500;
        background: color-mix(in srgb, #d6a500 10%, transparent);
        padding: 0.75rem 1rem;
        font-family: var(--brut-mono, monospace);
        font-size: 0.7rem;
    }

    .prompt strong {
        color: #9a7200;
        text-transform: uppercase;
        letter-spacing: 0.08em;
    }

    .lanes {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 1rem;
        margin-top: 1rem;
    }

    .lane {
        min-width: 0;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        padding: 0.875rem;
    }

    .lane header span {
        font-family: var(--brut-mono, monospace);
        font-size: 0.625rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    .clone header span,
    .clone h3 {
        color: #a24755;
    }

    .owned header span,
    .owned h3 {
        color: var(--brut-accent, #247768);
    }

    h3 {
        margin: 0.2rem 0 0;
        font-size: 1rem;
    }

    .stage {
        height: 150px;
        display: grid;
        place-items: center;
        margin: 0.75rem 0;
        border: 1px dashed var(--brut-rule-2, #bbc4c0);
        overflow: hidden;
    }

    :global(.card) {
        width: min(180px, calc(100% - 1.5rem));
        display: grid;
        gap: 0.625rem;
        border: 1px solid currentColor;
        background: var(--brut-bg-2, #eef4f1);
        padding: 0.75rem;
        box-shadow: 5px 5px 0 var(--brut-rule, #d6dedb);
    }

    :global(.clone .card) {
        color: #a24755;
    }

    :global(.owned .card) {
        color: var(--brut-accent, #247768);
    }

    input {
        min-width: 0;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg, #fff);
        color: var(--brut-ink, #0a0a0a);
        padding: 0.4rem;
        font: inherit;
    }

    button {
        width: 100%;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: transparent;
        color: var(--brut-ink, #0a0a0a);
        padding: 0.5rem;
        cursor: pointer;
    }

    .lane > p {
        min-height: 2.7rem;
        margin: 0.625rem 0 0;
        color: var(--brut-ink-2, #525252);
        font-size: 0.7rem;
        line-height: 1.45;
    }

    @media (max-width: 620px) {
        .lanes {
            grid-template-columns: 1fr;
        }

        .prompt {
            align-items: flex-start;
            flex-direction: column;
        }
    }
</style>
```
