Loading...
Loading...
Guide tasteful UI animation with easing, springs, layout animations, gestures, and accessibility. Covers Tailwind and Motion patterns. Use when: (1) Implementing enter/exit animations, (2) Choosing easing curves, (3) Configuring springs, (4) Layout animations and shared elements, (5) Drag/swipe gestures, (6) Micro-interactions, (7) Ensuring prefers-reduced-motion accessibility. Triggers: animate, animation, easing, spring, transition, motion, layout, gesture, drag, swipe, reduced motion, framer motion.
npx skill4agent add lukasstrickler/ai-dev-atelier ui-animationtransition-*animate-*AnimatePresencelayoutprefers-reduced-motion| Type | Examples | Technique |
|---|---|---|
| Micro-interaction | Button press, toggle, checkbox | CSS/Tailwind |
| Enter/Exit | Modal, toast, dropdown | Motion + AnimatePresence |
| Layout change | Accordion, reorder, expand | Motion |
| Shared element | Tab indicator, card expand | Motion |
| Gesture | Drag, swipe, pull-to-refresh | Motion springs |
references/recipes.mdAnimatePresenceui_to_artifactui_diff_checkprefers-reduced-motionuseReducedMotion()motion-safe:opacity: 0pointerEvents: nonetransformopacityWhat triggers the animation?
│
├─ User action (click, tap, open)?
│ └─ Use: ease-out (fast start, slow end = responsive)
│
├─ Element moving on-screen (tab switch, reorder)?
│ └─ Use: ease-in-out (accelerate then decelerate)
│
├─ Continuous/looping (spinner, marquee)?
│ └─ Use: linear (constant speed appropriate here)
│
├─ Gesture-based (drag, swipe, pull)?
│ └─ Use: Spring animation (physics-based, interruptible)
│
└─ Hover/focus effect?
└─ Use: CSS ease, 150ms (subtle, immediate)| Purpose | CSS | Tailwind | Duration |
|---|---|---|---|
| Modal/drawer enter | | | 200ms |
| Modal/drawer exit | | | 150ms |
| On-screen movement | | | 200-300ms |
| Hover effect | | | 150ms |
| Button press | — | | instant |
| Name | Value | Use Case |
|---|---|---|
| Vaul (buttery) | | Sheets, drawers, modals |
| Emphasized | | Material Design 3 |
| Snappy | | Fast UI transitions |
ease-in| Type | Duration | Notes |
|---|---|---|
| Micro-feedback | 100-150ms | Button press, toggle, checkbox |
| Small transition | 150-250ms | Tooltip, icon morph |
| Medium transition | 200-300ms | Modal, popover, dropdown |
| Large transition | 300-400ms | Page transition, complex layout |
| Maximum | <500ms | Exceptions: onboarding, data viz |
visualDurationbounce| Feel | Config | Use Case |
|---|---|---|
| Snappy | | Tabs, buttons, quick feedback |
| Standard | | Modals, menus, general UI |
| Gentle | | Smooth, human-like flow |
| Feel | Config | Use Case |
|---|---|---|
| Snappy | | High-frequency interactions |
| Standard | | Framer Handshake convention |
| Gentle | | react-motion preset |
Gotcha:/stiffness/dampingoverridesmass/duration. Pick one approach—don't mix.bounce
layoutlayoutlayout="position"| Prop Value | Effect | Use Case |
|---|---|---|
| Animates position AND size | Default for flexible elements |
| Animates only translation | Text/icons that shouldn't stretch |
| Animates only dimensions | Fixed-position expanding panels |
layoutIdlayoutIdlayoutId<LayoutGroup id="...">layout="position"layout<path>| Problem | Solution |
|---|---|
| Touch scroll conflicts | |
| Element snaps back | Check |
| Momentum feels wrong | |
| One-direction only | |
import { useReducedMotion } from "motion/react"
const shouldReduce = useReducedMotion()
const variants = shouldReduce
? { opacity: 1 } // Fade only
: { opacity: 1, scale: 1, y: 0 } // Full animationmotion-safe:animate-pulsemotion-reduce:transition-nonerequestAnimationFrame(() => ref.focus())aria-live| Standard | Size | Tailwind | Physical |
|---|---|---|---|
| Material Design | 48×48 dp | | ~9mm (recommended) |
| Apple HIG | 44×44 pt | | ~7mm |
| WCAG 2.2 (AA) | 24×24 px | | ~5mm (minimum) |
transformopacitywidthheighttopleftmarginpaddingwill-change20px: Avoid for real-time effects; use pre-blurred images instead
layoutanimate={{ height }}opacity: 0pointerEvents: "none"references/recipes.mdreferences/recipes.mdlayoutlayoutIdforceMountasChildlayoutwill-changepopLayoutforwardRefinitial={false}| Mode | Behavior | Use Case |
|---|---|---|
| Simultaneous enter/exit | Crossfades, overlays |
| Exit completes before enter | Page transitions, tabs |
| Exiting elements leave flow | List removals (with |
AnimatePresence// ❌ Exit never runs
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
// ✅ Wrap in AnimatePresence
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
</AnimatePresence><AnimatePresence initial={false}>| Don't | Do Instead | Why |
|---|---|---|
| | Avoids "popping" effect |
| | Linear feels robotic |
| Animations >500ms | Keep under 300ms | Feels sluggish |
| Same tooltip delay | First: 400ms, subsequent: 0ms | User mental model |
| Skip reduced-motion | Always | Accessibility |
| Animate layout props | Use | Performance |
| Excessive bounce | | Unprofessional |
Tailwind v4: Define keyframes viain CSS, not config.@theme
| Category | Classes |
|---|---|
| Enter | |
| Exit | |
| Timing | |
| Fill Mode | |
| When | Skill | Why |
|---|---|---|
| After implementing | | Ensure code passes checks |
| Reusable patterns | | Document component API |
| Before committing | | Use |
| Integration issues | | Look up latest patterns |
.ada/references/recipes.md