introducing duck motion
Enter/exit animations, mount/unmount transitions, and staggered sequences — built to pair with duck-variants and duck-primitives.
Why Another Animation Library?
Most animation libraries are general-purpose. They handle springs, keyframes, and timeline orchestration — powerful, but often more than you need when all you want is a smooth enter/exit transition on a dialog or dropdown.
@gentleduck/motion is purpose-built for UI component animations. It handles the hard part — coordinating mount/unmount with CSS or JS transitions — while staying small and composable.
What It Solves
The classic problem: you want a dialog to fade in when it opens and fade out when it closes. But React unmounts the component immediately when you set open={false}, so the exit animation never plays.
// The problem: component unmounts before animation finishes
{open && <Dialog>...</Dialog>}// The problem: component unmounts before animation finishes
{open && <Dialog>...</Dialog>}@gentleduck/motion solves this with a Presence primitive that delays unmounting until the exit animation completes.
How It Works
Loading diagram...
Motion primitives coordinate three phases:
- Mount — Component enters the DOM, enter animation triggers
- Present — Component is visible and interactive
- Exit — Exit animation plays, component unmounts after completion
Used Across the Ecosystem
Every animated component in @gentleduck/ui uses motion primitives:
| Component | Animation |
|---|---|
| Dialog | Fade + scale on open/close |
| Sheet | Slide from edge with backdrop fade |
| Drawer | Slide up with drag-to-dismiss |
| Dropdown Menu | Scale + fade from trigger |
| Tooltip | Fade with slight offset |
| Popover | Scale from anchor point |
| Collapsible | Height animation |
| Accordion | Height animation per section |
Pairing with Variants
Motion works naturally with @gentleduck/variants for conditional animation classes:
import { cva } from '@gentleduck/variants'
const overlay = cva('fixed inset-0 bg-black/50', {
variants: {
state: {
open: 'animate-in fade-in',
closed: 'animate-out fade-out',
},
},
})import { cva } from '@gentleduck/variants'
const overlay = cva('fixed inset-0 bg-black/50', {
variants: {
state: {
open: 'animate-in fade-in',
closed: 'animate-out fade-out',
},
},
})The variant system handles which classes to apply. Motion handles when to unmount.
Getting Started
bun add @gentleduck/motionbun add @gentleduck/motionMotion is a peer dependency of @gentleduck/primitives and is automatically included when you install components via the CLI. If you are building custom components on top of primitives, install it directly.