logo

AnimatePresence

AnimatePresence enables exit animations for components when they are removed from the DOM. Wrap your conditional content with AnimatePresence to animate children as they enter and exit.

<script>
  import { motion, AnimatePresence } from '@humanspeak/svelte-motion'

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

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="box"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>
<script>
  import { motion, AnimatePresence } from '@humanspeak/svelte-motion'

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

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="box"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>
Hello
State: visible

How it works

When a motion component inside AnimatePresence is conditionally removed:

  1. The component’s last position and styles are captured
  2. A visual clone is created at that exact position
  3. The exit animation runs on the clone
  4. Once complete, the clone is removed from the DOM

This approach ensures smooth exit animations even though the original Svelte component has already unmounted.

Usage

Basic enter/exit

The most common pattern is toggling visibility with enter and exit animations:

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

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

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
      transition={{ type: 'spring', stiffness: 300, damping: 25 }}
    >
      Modal content
    </motion.div>
  {/if}
</AnimatePresence>

<button onclick={() => isVisible = !isVisible}>
  Toggle
</button>
<script lang="ts">
  import { motion, AnimatePresence } from '@humanspeak/svelte-motion'

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

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
      transition={{ type: 'spring', stiffness: 300, damping: 25 }}
    >
      Modal content
    </motion.div>
  {/if}
</AnimatePresence>

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

The key prop

When using AnimatePresence, motion components need a key prop to track their identity:

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="unique-id"
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>
<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="unique-id"
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>

The key is used to:

  • Track which components are entering vs. exiting
  • Ensure the correct exit animation plays for each component
  • Handle re-entry animations correctly

Suppressing initial animation

By default, all children animate when AnimatePresence first mounts. To prevent this, set initial={false}:

<AnimatePresence initial={false}>
  {#if isVisible}
    <motion.div
      key="box"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>
<AnimatePresence initial={false}>
  {#if isVisible}
    <motion.div
      key="box"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  {/if}
</AnimatePresence>

With initial={false}:

  • Children present on first render skip their enter animation
  • Only subsequent entries (after exits) will animate in

Exit completion callback

Use onExitComplete to run code after all exit animations finish:

<script>
  function handleExitComplete() {
    console.log('All exits complete!')
    // Navigate, cleanup, etc.
  }
</script>

<AnimatePresence onExitComplete={handleExitComplete}>
  {#if isVisible}
    <motion.div key="box" exit={{ opacity: 0 }} />
  {/if}
</AnimatePresence>
<script>
  function handleExitComplete() {
    console.log('All exits complete!')
    // Navigate, cleanup, etc.
  }
</script>

<AnimatePresence onExitComplete={handleExitComplete}>
  {#if isVisible}
    <motion.div key="box" exit={{ opacity: 0 }} />
  {/if}
</AnimatePresence>

This is useful for:

  • Navigation after a page transition completes
  • Cleanup operations
  • Triggering subsequent animations

Working with lists

AnimatePresence works with dynamic lists using Svelte’s {#each} block:

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

  let items = $state([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ])

  function removeItem(id: number) {
    items = items.filter(item => item.id !== id)
  }
</script>

<AnimatePresence>
  {#each items as item (item.id)}
    <motion.div
      key={String(item.id)}
      initial={{ opacity: 0, x: -20 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 20 }}
    >
      {item.text}
      <button onclick={() => removeItem(item.id)}>Remove</button>
    </motion.div>
  {/each}
</AnimatePresence>
<script lang="ts">
  import { motion, AnimatePresence } from '@humanspeak/svelte-motion'

  let items = $state([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ])

  function removeItem(id: number) {
    items = items.filter(item => item.id !== id)
  }
</script>

<AnimatePresence>
  {#each items as item (item.id)}
    <motion.div
      key={String(item.id)}
      initial={{ opacity: 0, x: -20 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 20 }}
    >
      {item.text}
      <button onclick={() => removeItem(item.id)}>Remove</button>
    </motion.div>
  {/each}
</AnimatePresence>

Important: Use Svelte’s keyed {#each} block ({#each items as item (item.id)}) and pass a matching string key prop to the motion component.

Combining with variants

AnimatePresence works seamlessly with variants for cleaner, reusable animations:

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

  let isVisible = $state(true)

  const itemVariants: Variants = {
    hidden: { opacity: 0, y: 20 },
    visible: { opacity: 1, y: 0 },
    exit: { opacity: 0, y: -20 }
  }
</script>

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="item"
      variants={itemVariants}
      initial="hidden"
      animate="visible"
      exit="exit"
    />
  {/if}
</AnimatePresence>
<script lang="ts">
  import { motion, AnimatePresence, type Variants } from '@humanspeak/svelte-motion'

  let isVisible = $state(true)

  const itemVariants: Variants = {
    hidden: { opacity: 0, y: 20 },
    visible: { opacity: 1, y: 0 },
    exit: { opacity: 0, y: -20 }
  }
</script>

<AnimatePresence>
  {#if isVisible}
    <motion.div
      key="item"
      variants={itemVariants}
      initial="hidden"
      animate="visible"
      exit="exit"
    />
  {/if}
</AnimatePresence>

Exit transition configuration

You can specify a custom transition for the exit animation:

<motion.div
  key="box"
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{
    opacity: 0,
    scale: 0.8,
    transition: { duration: 0.2, ease: 'easeIn' }
  }}
/>
<motion.div
  key="box"
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{
    opacity: 0,
    scale: 0.8,
    transition: { duration: 0.2, ease: 'easeIn' }
  }}
/>

The exit transition takes precedence over the component’s general transition prop.

Props

AnimatePresence

PropTypeDefaultDescription
initialbooleantrueWhen false, children skip their enter animation on initial mount
onExitComplete() => voidundefinedCallback invoked once all exit animations complete

Motion component props (for AnimatePresence)

PropTypeDescription
keystringRequired. Unique identifier for tracking enter/exit state
initialobject \| string \| falseInitial animation state
animateobject \| stringTarget animation state
exitobject \| stringExit animation state when component is removed

Best practices

1. Always provide a key

Every motion component inside AnimatePresence should have a unique key:

<!-- Good -->
<motion.div key="modal" exit={{ opacity: 0 }} />

<!-- Bad - no key -->
<motion.div exit={{ opacity: 0 }} />
<!-- Good -->
<motion.div key="modal" exit={{ opacity: 0 }} />

<!-- Bad - no key -->
<motion.div exit={{ opacity: 0 }} />

2. Use semantic exit animations

Exit animations should feel natural and match the context:

<!-- Modal: scale down and fade -->
<motion.div
  key="modal"
  exit={{ opacity: 0, scale: 0.95 }}
/>

<!-- Sidebar: slide out -->
<motion.div
  key="sidebar"
  exit={{ x: -300 }}
/>

<!-- Toast: slide up and fade -->
<motion.div
  key="toast"
  exit={{ opacity: 0, y: -20 }}
/>
<!-- Modal: scale down and fade -->
<motion.div
  key="modal"
  exit={{ opacity: 0, scale: 0.95 }}
/>

<!-- Sidebar: slide out -->
<motion.div
  key="sidebar"
  exit={{ x: -300 }}
/>

<!-- Toast: slide up and fade -->
<motion.div
  key="toast"
  exit={{ opacity: 0, y: -20 }}
/>

3. Match enter and exit

For visual consistency, exit animations often mirror enter animations:

<motion.div
  key="item"
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: 20 }}  <!-- Same as initial -->
/>
<motion.div
  key="item"
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: 20 }}  <!-- Same as initial -->
/>

4. Consider performance

Keep exit animations short (200-400ms) to avoid UI feeling sluggish:

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

Common patterns

Page transitions

<script>
  import { page } from '$app/stores'
</script>

<AnimatePresence>
  {#key $page.url.pathname}
    <motion.main
      key={$page.url.pathname}
      initial={{ opacity: 0, x: 20 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: -20 }}
    >
      <slot />
    </motion.main>
  {/key}
</AnimatePresence>
<script>
  import { page } from '$app/stores'
</script>

<AnimatePresence>
  {#key $page.url.pathname}
    <motion.main
      key={$page.url.pathname}
      initial={{ opacity: 0, x: 20 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: -20 }}
    >
      <slot />
    </motion.main>
  {/key}
</AnimatePresence>

Modal/Dialog

<AnimatePresence>
  {#if showModal}
    <!-- Backdrop -->
    <motion.div
      key="backdrop"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      class="backdrop"
      onclick={() => showModal = false}
    />

    <!-- Modal -->
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.9, y: 20 }}
      animate={{ opacity: 1, scale: 1, y: 0 }}
      exit={{ opacity: 0, scale: 0.9, y: 20 }}
      class="modal"
    >
      Modal content
    </motion.div>
  {/if}
</AnimatePresence>
<AnimatePresence>
  {#if showModal}
    <!-- Backdrop -->
    <motion.div
      key="backdrop"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      class="backdrop"
      onclick={() => showModal = false}
    />

    <!-- Modal -->
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.9, y: 20 }}
      animate={{ opacity: 1, scale: 1, y: 0 }}
      exit={{ opacity: 0, scale: 0.9, y: 20 }}
      class="modal"
    >
      Modal content
    </motion.div>
  {/if}
</AnimatePresence>

Notification stack

<AnimatePresence>
  {#each notifications as notification (notification.id)}
    <motion.div
      key={notification.id}
      initial={{ opacity: 0, x: 100 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 100 }}
      transition={{ type: 'spring', stiffness: 500, damping: 30 }}
    >
      {notification.message}
    </motion.div>
  {/each}
</AnimatePresence>
<AnimatePresence>
  {#each notifications as notification (notification.id)}
    <motion.div
      key={notification.id}
      initial={{ opacity: 0, x: 100 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 100 }}
      transition={{ type: 'spring', stiffness: 500, damping: 30 }}
    >
      {notification.message}
    </motion.div>
  {/each}
</AnimatePresence>

Related


Based on Motion’s AnimatePresence API.