ui-animation
UI Animation
Tasteful UI animation with proper timing, accessibility, and performance.
Quick Start
Technique Decision:
- Simple transition? → CSS/Tailwind (
transition-*,animate-*) - Enter/exit with unmount? → Motion +
AnimatePresence - Gesture-driven? → Motion springs
- Layout changes? → Motion
layoutprop
Default: Start with Easing Decision Tree → Duration Guidelines → Implement → Add a11y → Verify
Core Principles
- Natural Motion: Mimic physics. Avoid linear easing - nothing moves at constant speed.
- Purposeful: Every animation must add meaning. If you can't explain its benefit, remove it.
- Fast: UI animations under 300ms. Hover effects under 150ms. Over 500ms feels sluggish.
- Interruptible: Use springs for gesture-driven animations - they handle interruption gracefully.
- Accessible: Always respect
prefers-reduced-motion. Non-negotiable.
Workflow
Step 1: Classify Animation Type
| Type | Examples | Technique |
|---|---|---|
| Micro-interaction | Button press, toggle, checkbox | CSS/Tailwind |
| Enter/Exit | Modal, toast, dropdown | Motion + AnimatePresence |
| Layout change | Accordion, reorder, expand | Motion layout prop |
| Shared element | Tab indicator, card expand | Motion layoutId |
| Gesture | Drag, swipe, pull-to-refresh | Motion springs |
Step 2: Choose Timing
- Use Easing Decision Tree below to select curve
- Use Duration Guidelines to select timing
- For gestures, use Spring Animations config
Step 3: Implement
- Check
references/recipes.mdfor copy-paste patterns - Apply timing from Step 2
- Wrap unmounting elements in
AnimatePresence
Use ui_to_artifact when starting from a design screenshot or mockup. Use ui_diff_check to compare expected vs implemented UI.
Step 4: Accessibility (Required)
- Check
prefers-reduced-motionwithuseReducedMotion()ormotion-safe: - Simplify to opacity-only for reduced motion users
- Verify focus timing (move focus AFTER animation starts)
Step 5: Verify
- Exit animations run (not instant unmount)
-
opacity: 0elements havepointerEvents: none - Focus moves after animation starts, not before
- Only animating
transformandopacity
Easing
Decision Tree
What 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)
Quick Reference
| Purpose | CSS | Tailwind | Duration |
|---|---|---|---|
| Modal/drawer enter | cubic-bezier(0.32, 0.72, 0, 1) |
ease-out duration-200 |
200ms |
| Modal/drawer exit | cubic-bezier(0.32, 0.72, 0, 1) |
ease-out duration-150 |
150ms |
| On-screen movement | cubic-bezier(0.4, 0, 0.2, 1) |
ease-in-out duration-200 |
200-300ms |
| Hover effect | ease |
ease duration-150 |
150ms |
| Button press | — | active:scale-[0.97] |
instant |
Pro Curves
| Name | Value | Use Case |
|---|---|---|
| Vaul (buttery) | cubic-bezier(0.32, 0.72, 0, 1) |
Sheets, drawers, modals |
| Emphasized | cubic-bezier(0.2, 0, 0, 1) |
Material Design 3 |
| Snappy | cubic-bezier(0.25, 1, 0.5, 1) |
Fast UI transitions |
Avoid: Built-in ease-in—starts slow, feels sluggish.
Duration Guidelines
| 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 |
Key Rules:
- Exit faster than enter: 200ms enter → 150ms exit
- Hover = fast: Under 150ms
- High-frequency = instant: Keyboard nav, scrolling—<100ms or none
Spring Animations
Duration-based (Recommended)
Easier to compose with other timed animations. Use visualDuration (time to visually reach target) and bounce (0 = no bounce, 1 = very bouncy).
| Feel | Config | Use Case |
|---|---|---|
| Snappy | { duration: 0.3, bounce: 0.15 } |
Tabs, buttons, quick feedback |
| Standard | { duration: 0.4, bounce: 0.2 } |
Modals, menus, general UI |
| Gentle | { duration: 0.5, bounce: 0.25 } |
Smooth, human-like flow |
Physics-based (Legacy/Advanced)
Use when integrating with physics libraries or when precise control over spring dynamics is needed.
| Feel | Config | Use Case |
|---|---|---|
| Snappy | { stiffness: 400, damping: 30 } |
High-frequency interactions |
| Standard | { stiffness: 300, damping: 20 } |
Framer Handshake convention |
| Gentle | { stiffness: 120, damping: 14 } |
react-motion preset |
Gotcha:
stiffness/damping/massoverridesduration/bounce. Pick one approach—don't mix.
Layout Animations
The layout Prop
Add layout to animate position/size changes automatically. Use layout="position" for text (prevents distortion).
| Prop Value | Effect | Use Case |
|---|---|---|
layout={true} |
Animates position AND size | Default for flexible elements |
layout="position" |
Animates only translation | Text/icons that shouldn't stretch |
layout="size" |
Animates only dimensions | Fixed-position expanding panels |
Shared Element Transitions (layoutId)
Elements with matching layoutId animate between each other when entering/exiting.
Critical Trap: Duplicate layoutId values cause elements to teleport across the page. Use unique IDs per context or wrap in <LayoutGroup id="...">.
Layout Gotchas
- Text distortion: Apply
layout="position"to text elements - Border radius: Can warp during scale—Motion auto-corrects, but test it
- SVG elements:
layoutdoesn't work on<path>—use manual morphing
Gesture Gotchas
| Problem | Solution |
|---|---|
| Touch scroll conflicts | dragPropagation={false} |
| Element snaps back | Check dragConstraints + dragElastic |
| Momentum feels wrong | dragMomentum={false} for precise UIs |
| One-direction only | dragElastic={{ top: 0, bottom: 0.5 }} |
Swipe dismiss: Check BOTH distance AND velocity—users expect flicks to work.
Accessibility
prefers-reduced-motion (REQUIRED)
import { useReducedMotion } from "motion/react"
const shouldReduce = useReducedMotion()
const variants = shouldReduce
? { opacity: 1 } // Fade only
: { opacity: 1, scale: 1, y: 0 } // Full animation
Tailwind: motion-safe:animate-pulse / motion-reduce:transition-none
Best practice: Don't disable—simplify. Remove spatial movement, keep opacity.
Focus Management
- Move focus AFTER animation starts:
requestAnimationFrame(() => ref.focus()) - Restore focus to trigger on close
- Don't animate inside
aria-liveregions
Touch Targets
| Standard | Size | Tailwind | Physical |
|---|---|---|---|
| Material Design | 48×48 dp | min-h-12 min-w-12 |
~9mm (recommended) |
| Apple HIG | 44×44 pt | min-h-11 min-w-11 |
~7mm |
| WCAG 2.2 (AA) | 24×24 px | min-h-6 min-w-6 |
~5mm (minimum) |
Why? Average adult finger pad is ~9mm. Targets below 7mm cause "fat finger" errors. Use Material's 48dp for cross-platform; Apple's 44pt is iOS-specific minimum.
Performance
Golden Rules
- Only animate
transformandopacity—GPU-accelerated - Never animate:
width,height,top,left,margin,padding will-changesparingly—only during animation, remove after- Blur thresholds:
- ≤10px: Safe for animation
- 11-20px: May cause jank on mobile/4K—test thoroughly
-
20px: Avoid for real-time effects; use pre-blurred images instead
- Prefer CSS over JS for simple transitions
Key Traps
- Height animation: Use
layoutprop, notanimate={{ height }} - Invisible but clickable:
opacity: 0still receives clicks—addpointerEvents: "none" - will-change everywhere: Causes layer explosion, mobile crashes
See references/recipes.md for detailed examples.
Examples
Copy-paste patterns organized by category in references/recipes.md:
- Common UI Patterns: Button press, modal enter/exit, error shake, staggered lists, accordion
- Touch & Interaction: Accessible touch targets, hover on touch devices, instant tooltips
- Layout Animations:
layoutprop,layoutIdshared elements, collision fixes - Radix UI Integration:
forceMountpattern,asChild, origin-aware popovers - Accessibility: Focus timing, focus restoration, reduced motion variants
- Performance: Height animation (use
layout), invisible-but-clickable fix,will-change - Exit Patterns:
popLayoutwithforwardRef, SSR hydration (initial={false}) - Gestures: Swipe dismiss with velocity check, elastic drag boundaries
AnimatePresence
| Mode | Behavior | Use Case |
|---|---|---|
sync (default) |
Simultaneous enter/exit | Crossfades, overlays |
wait |
Exit completes before enter | Page transitions, tabs |
popLayout |
Exiting elements leave flow | List removals (with layout) |
Exit Animation Trap
Exit animations require AnimatePresence—without it, unmount is instant:
// ❌ Exit never runs
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
// ✅ Wrap in AnimatePresence
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
</AnimatePresence>
SSR: Use <AnimatePresence initial={false}> to prevent animation on page load.
Anti-patterns
| Don't | Do Instead | Why |
|---|---|---|
scale(0) start |
scale(0.9) or higher |
Avoids "popping" effect |
linear for UI |
ease-out or springs |
Linear feels robotic |
| Animations >500ms | Keep under 300ms | Feels sluggish |
| Same tooltip delay | First: 400ms, subsequent: 0ms | User mental model |
| Skip reduced-motion | Always motion-safe: |
Accessibility |
| Animate layout props | Use transform: scale() |
Performance |
| Excessive bounce | bounce: 0-0.2 |
Unprofessional |
tailwindcss-animate
Tailwind v4: Define keyframes via
@themein CSS, not config.
| Category | Classes |
|---|---|
| Enter | animate-in fade-in zoom-in-95 slide-in-from-top |
| Exit | animate-out fade-out zoom-out-95 slide-out-to-top |
| Timing | delay-150 duration-500 |
| Fill Mode | fill-mode-forwards fill-mode-backwards |
Integration with Other Skills
| When | Skill | Why |
|---|---|---|
| After implementing | code-quality |
Ensure code passes checks |
| Reusable patterns | docs-write |
Document component API |
| Before committing | git-commit |
Use feat(ui): or style: |
| Integration issues | search |
Look up latest patterns |
Output
- Artifacts: Code changes only (no
.ada/outputs) - Modifications: Component animations, CSS/Tailwind styles, Motion configs
- Type: Workflow skill (guidance only, no scripts)
References
Internal
references/recipes.md- Copy-paste patterns, integration examples, detailed traps
External
- Motion Documentation
- Material Design 3 Motion
- Apple HIG - Motion
- tailwindcss-animate
- easings.net - Easing function cheat sheet
More from lukasstrickler/ai-dev-atelier
use-graphite
Manage stacked PRs with Graphite CLI (gt) instead of git push/gh pr create. Auto-detects Graphite repos and blocks conflicting commands with helpful alternatives. Use when: (1) About to run git push or gh pr create in a Graphite repo, (2) Creating a new branch for a feature, (3) Submitting code for review, (4) Large changes that should be split into reviewable chunks, (5) Hook blocks your git command and suggests gt equivalent. NOT for: repos not initialized with Graphite, git add/commit/status/log. Triggers: git push blocked, gh pr create blocked, create branch, submit PR, stacked PRs, split large PR, gt create, gt submit, graphite workflow.
14tdd
Strict Red-Green-Refactor workflow for robust, self-documenting code. Discovers project test setup via codebase exploration before assuming frameworks. Use when: (1) Implementing new features with test-first approach, (2) Fixing bugs with reproduction tests, (3) Refactoring existing code with test safety net, (4) Adding tests to legacy code, (5) Ensuring code quality before committing, (6) When tests exist but workflow unclear, or (7) When establishing testing practices in a new project. Triggers: test, tdd, red-green-refactor, failing test, test first, test-driven, write tests, add tests, run tests.
9code-quality
Run comprehensive code quality checks including TypeScript typecheck, ESLint linting, Prettier formatting, and Markdown validation. Auto-fixes formatting issues in agent mode or provides read-only checks for CI pipelines. Use when: (1) Before committing code changes, (2) In CI/CD pipelines for automated quality gates, (3) After making significant code changes, (4) When preparing code for review, (5) When ensuring code meets quality standards, (6) For type checking, linting, formatting, and markdown validation, (7) In pre-commit hooks, or (8) For automated quality gates before merging. Triggers: finalize, code quality, typecheck, lint, format, check code, quality check, run checks, pre-commit, before commit, CI checks, validate code.
9git-commit
Write clear git commits with Conventional Commits format. Detects project conventions from history and config. Guides commit granularity. Use when: (1) Completing working code, (2) Code builds and tests pass, (3) Ready to save, (4) Before pushing, (5) After review feedback. Triggers: automatically when finishing commitable work that builds and passes tests.
8image-generation
Generate, edit, and upscale AI images. Use when creating visual assets for apps, websites, or documentation. FREE Cloudflare tier for iterate generation (~96/day), Fal.ai for paid tiers. Four quality tiers (iterate/default/premium/max). Supports text specialists, multi-ref editing, SVG, background removal. Triggers: generate image, create image, edit image, upscale, logo, picture of, remove background.
7docs-write
Write or update documentation (tutorial, how-to, reference, explanation) with clear style, structure, visuals, API/ADR/runbook patterns. Use when: (1) Creating or updating docs after code changes, (2) During PR preparation or addressing review feedback, (3) Adding new features that need documentation, (4) Updating API endpoints, database schemas, or configuration, (5) Creating ADRs or runbooks, (6) Adding or updating diagrams and visual documentation, (7) When documentation needs to be written or revised, (8) For tutorial creation, how-to guides, or technical writing, or (9) For documentation standards compliance and structure. Triggers: write docs, update documentation, create documentation, write tutorial, document API, write ADR, create runbook, add documentation, document this, write how-to.
7