| name | motion-principles |
| description | Purposeful motion for mobile UIs — easing, duration scales, and reduced-motion. Use this when designing or reviewing animations. |
Motion Principles
Instructions
Motion communicates causality, hierarchy, and feedback. If it does not do one of those three things, it is noise. Every animation in the design system must have an intent, a token-backed duration, and a reduced-motion fallback.
1. Purpose Before Polish
Every motion answers one question:
- Causality — what caused this change? (Sheet rises from the button that opened it.)
- Hierarchy — what is primary vs secondary? (Stagger onboarding items 30ms apart.)
- Feedback — did my input register? (80ms press scale, 200ms state transition.)
If none apply, ship a cut, not a transition.
2. Duration Scale
Small, named scale. No magic milliseconds in component code.
| Token | ms | Use |
|---|
motion.duration.instant | 0 | Reduced-motion fallback |
motion.duration.xs | 80 | Micro-feedback (press, toggle) |
motion.duration.sm | 150 | Small UI state changes, tooltip |
motion.duration.md | 250 | Surface enter/exit, sheet, drawer |
motion.duration.lg | 400 | Hero / shared element |
motion.duration.xl | 600 | Onboarding, first-run reveal |
Rule: most durations in a mobile app live between 150–300ms. Anything longer than 400ms needs justification.
3. Easing Scale
| Token | Curve | Use |
|---|
motion.easing.standard | cubic-bezier(0.2, 0.0, 0, 1.0) | Enter + exit balanced |
motion.easing.emphasized | cubic-bezier(0.2, 0.0, 0, 1.0) with longer duration | Emphasize arrival |
motion.easing.accelerate | cubic-bezier(0.3, 0.0, 1, 1) | Exit off-screen |
motion.easing.decelerate | cubic-bezier(0, 0, 0, 1) | Enter from off-screen |
motion.easing.linear | linear | Opacity, crossfades, loading |
Prefer emphasized easing (Material 3) for full-screen transitions; it feels purposeful without being theatrical.
4. Stagger & Choreography
Stagger lists with 20–40ms between items, capped at ~6 visible items (beyond that, reveal the remainder without stagger). Never stagger in a loading state — it implies progress where there is none.
5. Reduced Motion
Honor the system preference always.
- Compose:
LocalAccessibilityManager.current or Settings.Global.TRANSITION_ANIMATION_SCALE.
- SwiftUI:
@Environment(\.accessibilityReduceMotion).
- Flutter:
MediaQuery.disableAnimations / MediaQueryData.accessibleNavigation.
- React Native:
AccessibilityInfo.isReduceMotionEnabled().
When enabled, motion tokens collapse:
export function resolveMotion(base: Motion, reduce: boolean): Motion {
if (!reduce) return base;
return { ...base, duration: { instant: 0, xs: 0, sm: 0, md: 0, lg: 0, xl: 0 } };
}
Replace translation/scale animations with instant changes; keep opacity crossfades under 150ms for state legibility.
6. Physics vs Tween
- Use tween + easing for state transitions where start and end are known (open/close, show/hide).
- Use spring physics for direct manipulation (drag-to-dismiss, swipe-to-delete, pull-to-refresh). Tokenize stiffness/damping if springs are a system-level choice.
.animation(.spring(response: 0.35, dampingFraction: 0.82), value: isOpen)
7. Don't Animate These
- Keyboard appearance/dismissal (OS owns it).
- Navigation transitions between stacks (platform owns them — iOS push, Android predictive back).
- Incoming network data that can arrive in bursts (use a subtle fade, not a slide).
- Text color during dark-mode switch — crossfade the whole surface instead.
8. Anti-Patterns
Thread.sleep(300) or setTimeout(300) to coordinate animations — use completion callbacks / withAnimation.
- Hardcoded 300ms everywhere — breaks the duration scale.
- Animation without a reduced-motion fallback.
- Staggered loaders.
- "Juicy" bounces on enterprise flows (banking, medical) — erodes trust.
Checklist