Skip to content

Motion

Animation is a quiet feedback channel. We use it to make transitions feel intentional, never to entertain.

Easing

A single curve covers ~95% of UI motion:

cubic-bezier(0.4, 0, 0.2, 1)

It's the standard "ease-in-out" — fast in the middle, slow at both ends. Reads as confident, not bouncy.

Reserve other curves for specific cases:

Use Curve Why
Default UI transitions cubic-bezier(0.4, 0, 0.2, 1) Symmetric ease, feels neutral and intentional
Entrance (mount) animations cubic-bezier(0, 0, 0.2, 1) Ease-out — accelerates in, settles
Exit (dismiss) animations cubic-bezier(0.4, 0, 1, 1) Ease-in — settles in place, accelerates out
Springy / playful (rare — only VoxelSlot and onboarding) spring(damping: 12, stiffness: 200) Reserved for brand-moment surfaces, never product chrome

Duration

Three named durations.

Token ms Use
fast 150 Hover/press states, focus rings, instant feedback
base 240 Most transitions — fades, slides, height changes
slow 360 Page-level transitions, sheet present/dismiss, modal in/out

No transition should exceed 400ms. If it needs to be longer, it's either a marketing animation (different rules apply) or the wrong solution.

Common patterns

Fade-in on mount

FadeInView wraps content and fades from opacity: 0 to 1 over base (240ms) with the entrance ease curve. Use for first-mount of content that arrives async.

Press feedback

Buttons darken their background by ~5% on press, with a fast (150ms) transition. No scale-down, no haptic on web. On native, a single Haptics.selectionAsync() fires on press-in.

Sheet present/dismiss

Bottom sheets slide up from the bottom over slow (360ms) with the entrance ease. Dismiss runs the inverse with the exit ease. The backdrop fades from 0 to 40% black over the same duration.

Hover (web only)

On web, interactive elements respond to hover with a fast background shift to the next surface tier (e.g. surfacesurfaceContainerLow). No transforms, no glow.

Reduced motion

Always respect the user's OS-level setting. On native, check AccessibilityInfo.isReduceMotionEnabled(). On web, check prefers-reduced-motion: reduce. When reduce-motion is on:

  • All non-functional animation (fades, slides) becomes instant.
  • Spring animations become linear ease.
  • Parallax / scroll-reveal effects disable entirely.

Wrap any custom animation in a useReducedMotion() hook (in frontend/hooks/useReducedMotion.ts — when added) rather than scattering the check across components.

Things to avoid

  • Stagger animations on lists. "Each item slides in 50ms after the last" feels showy and slows perceived load time. Render synchronously, fade the container.
  • Infinite spinners as primary loading state. Use skeleton screens (matching final layout) or progress bars when duration is knowable. Spinners only for sub-second indeterminate waits.
  • Bounce on errors. Don't shake form fields. Show the error message in error color with a fast fade-in.
  • Easter-egg animations. No confetti, no "you did it!" cascades. Tomoda's voice doesn't celebrate at the user.

Where motion is defined

Currently inline in components via Animated.timing / Animated.spring (React Native) and transition: ... CSS (web). When a motion token (duration or easing) gets reused enough to warrant centralization, add it to a frontend/constants/Motion.ts file and update this page in the same PR.