Lesson 6: Animation with Presence
Use Presence for reliable enter/exit motion, forceMount integration, and reduced-motion compliance.
Lesson 6 of 10: Great motion is not decoration. It must respect lifecycle, accessibility, and user settings.
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:
mountedunmountSuspendedunmounted
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:
- Verify closed-state animation exists.
- Verify selector matches rendered element.
- Verify no immediate
display: nonestyle is applied. - Verify
forceMountstrategy if using JS animation library.
Lab
- Add side-aware popover motion.
- Add dialog overlay/content enter and exit motion.
- Validate behavior with reduced-motion enabled.