JS-driven animation in web/mobile uses the motion package through the shared
presets module src/lib/motion/presets.ts. Components never define their own
durations, easings, or springs.
The presets
Consume via the hook (reduced-motion aware — this is mandatory):
import {AnimatePresence, motion} from "motion/react"
import {useMotionPresets} from "@/lib/motion/presets"
const presets = useMotionPresets()
In use today:
crossfade— the only preset components consume. Used for the reply reveal (features/chat/AnswerReveal.tsx) and the composer's picker and recording overlays (features/chat/Composer.tsx). For skeleton → content swaps, skeleton and content must occupy identical geometry so the fade causes zero layout shift.
Defined but not used by any screen yet:
sharedAxisPush— horizontal parent → child push.custom={1}forward,custom={-1}back, inside<AnimatePresence custom={direction} initial={false}>.sheetSlideUp— spring-based slide-up for a custom bottom sheet.- The raw
pushTransition/sheetTransition/crossfadeTransitionvalues and thereducedflag.
Reach for these before adding a new preset. Screen navigation has no transition
today, and sheets and drawers (Sheet from @agenta/ui/ui or the local
src/components/ui/sheet.tsx) animate with their own Tailwind
animate-in/slide-in-* classes, not with motion.
Rules
- Animate navigation, containment, and state swaps — not decoration. No
attention-seeking motion, no animating properties that trigger layout
(animate
transform/opacityonly). - Reduced motion is not optional.
useMotionPresets()returns instant variants and zero-duration transitions whenprefers-reduced-motionis set. Any animation built outside the presets module must justify itself in review AND handle reduced motion itself. CSS keyframe animations need a@media (prefers-reduced-motion: reduce)override inglobals.css(see.animate-composer-ring). Prefer extending the presets module. - Message entrance/streaming: subtle — the reply fades in on
crossfade; text streaming is never per-character animated. - New shared patterns go INTO
presets.ts(one exported preset + doc comment, plus its reduced variant inuseMotionPresets), not into a component file.