Skip to main content

Lesson 6: Animation with Presence

Use Presence for reliable enter/exit motion, forceMount integration, and reduced-motion compliance.

Why Presence exists

A common bug in animated overlays: component unmounts before exit animation finishes.

Presence solves this by delaying unmount until animation completion.

import { Presence } from '@gentleduck/primitives/presence'
 
<Presence present={open}>
  <div className={open ? 'animate-in' : 'animate-out'} />
</Presence>
import { Presence } from '@gentleduck/primitives/presence'
 
<Presence present={open}>
  <div className={open ? 'animate-in' : 'animate-out'} />
</Presence>

State machine mental model

Presence transitions through:

  1. mounted
  2. unmountSuspended
  3. unmounted

When present becomes false, it checks animation state and waits for completion.


CSS contract for primitives

Most primitives expose data-state="open|closed".

.dialog-content[data-state='open'] { animation: zoomIn 160ms ease; }
.dialog-content[data-state='closed'] { animation: zoomOut 120ms ease; }
.dialog-content[data-state='open'] { animation: zoomIn 160ms ease; }
.dialog-content[data-state='closed'] { animation: zoomOut 120ms ease; }

Always define both states when using animated exit.


Using JS animation libraries

Use forceMount when a motion library controls render lifecycle.

<Dialog.Portal forceMount>
  <Dialog.Content forceMount asChild>
    <motion.div />
  </Dialog.Content>
</Dialog.Portal>
<Dialog.Portal forceMount>
  <Dialog.Content forceMount asChild>
    <motion.div />
  </Dialog.Content>
</Dialog.Portal>

This avoids double lifecycle ownership.


Reduced motion policy

Respect system preference:

@media (prefers-reduced-motion: reduce) {
  .dialog-overlay,
  .dialog-content {
    animation: none !important;
  }
}
@media (prefers-reduced-motion: reduce) {
  .dialog-overlay,
  .dialog-content {
    animation: none !important;
  }
}

Motion should never block usability.


Debugging animation issues

If exit motion is not visible:

  1. Verify closed-state animation exists.
  2. Verify selector matches rendered element.
  3. Verify no immediate display: none style is applied.
  4. Verify forceMount strategy if using JS animation library.

Lab

  1. Add side-aware popover motion.
  2. Add dialog overlay/content enter and exit motion.
  3. Validate behavior with reduced-motion enabled.