Skip to content

Motion — Physics

themes specs/themes/motion/physics.kmd

Motion physics for Koder UI — principles, performance budget, forbidden patterns, reduced-motion contract, and spring tokens (Material 3 Expressive). The "how it moves" half of Motion (the "how long" half lives in `easing-duration.kmd`).

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Motion: Physics

Facet Visual do Koder Design. Material parity: https://m3.material.io/styles/motion/overview/how-it-works.

Principles

  1. Motion has a reason — state change, hierarchy, spatial cue. Decorative animation (idle particles, ambient parallax) prohibited in app UI; allowed in marketing landings only.
  2. Fast enough to disappear — UI animations are sub-300ms. Above that, the user notices "the app is animating" instead of "the app responded."
  3. Easing matches intent — entering uses decelerate (fast→slow, "arriving"); exiting uses accelerate (slow→fast, "leaving"); on-screen tweaks use ease-in-out. Detail: easing-duration.kmd.
  4. Reduced motion respected — every animation collapses to instant (0ms) under prefers-reduced-motion: reduce.

R1 — Reduced motion contract

When prefers-reduced-motion: reduce:

  • All durations → 0ms (instant transitions)
  • Scale animations → opacity only (no scaling)
  • Continuous animations → static (loaders use indeterminate progress text instead of spinning)
  • Parallax → disabled
  • Auto-playing carousels → static, manual advance only
  • Spring animations → snap to end state (0ms); no overshoot, no oscillation

CSS implementation:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0ms !important;
    scroll-behavior: auto !important;
  }
}

R2 — Performance budget

  • All transitions composited via GPU (transform, opacity)
  • Avoid animating: width, height, top, left, padding, margin (causes layout reflow)
  • Single-frame check: animations stay at 60fps on mid-tier mobile
  • Coordinated multi-element animations: stagger (50-100ms offset) so the GPU doesn't paint all frames simultaneously

R3 — Forbidden patterns

  • ❌ Animations > 700ms (except marketing hero)
  • ❌ Bounce/elastic curves in UI (only marketing landings) — see R4 spring exception
  • ❌ Auto-playing animations on tab return (cumulative motion blur)
  • ❌ Animating colors via JS (use CSS transitions)
  • animation: ... infinite outside loaders / static decoration in landings

R4 — Spring tokens (Material 3 Expressive)

Duration-based tokens (easing-duration.kmd) describe how long an animation takes; spring tokens describe how it moves — physics-driven (stiffness, damping ratio, mass) instead of time-driven. Spring physics is what unlocks Expressive: interruptible gestures, natural overshoot/ bounce on movement, smooth chaining of state changes mid-animation.

Two families, separated by what they animate:

R4.1 — Spatial tokens (movement, size, position)

Used for any property that has a spatial meaning — translate, scale, layout size, rotation, container shape morph. May overshoot slightly (natural physics) — bounded so the overshoot reads as "settled" not "jiggle."

TokenStiffnessDamping ratioMassUse
motion-spatial-fast14000.91.0Small element movement (chip select, switch thumb)
motion-spatial-default7000.91.0Standard UI translate (drawer, sheet, FAB position)
motion-spatial-slow3000.81.0Hero element settle, container transform

R4.2 — Effect tokens (fade, color, opacity)

Used for properties that are non-spatial — opacity, color channel, blur radius, elevation alpha. High damping ratio (≥1.0) guarantees zero overshoot (a half-faded element bouncing back into visibility is wrong).

TokenStiffnessDamping ratioMassUse
motion-effect-fast38001.01.0Hover/press color tween, ripple fade
motion-effect-default16001.01.0Standard opacity/color crossfade
motion-effect-slow8001.01.0Hero crossfade, scrim reveal

R4.3 — Decision tree: duration token vs spring token

Is the animation INTERRUPTIBLE mid-flight (drag, fling, gesture)?
├── YES → use spring (R4.1 for spatial, R4.2 for effect)
└── NO  → does it move position/size?
          ├── YES, and it has natural physics (FAB morph, sheet snap)
          │       → use spring (R4.1)
          └── NO, it's a deterministic timed transition (modal entry,
                  cross-fade, loader)
                  → use duration tokens (`easing-duration.kmd` R1-R2)

Spring tokens are additive to duration tokens — duration tokens remain canonical for deterministic transitions. New code should prefer springs when the motion is gestural or spatial; legacy duration use stays valid.

R4.4 — Surface bindings

SurfaceAPI
FlutterSpringSimulation + SpringDescription(mass, stiffness, damping)
Composespring(stiffness=, dampingRatio=) from androidx.compose.animation.core
SwiftUI.spring(response:, dampingFraction:, blendDuration:) (response = 2π√(mass/stiffness))
WebCSS linear() easing precomputed from spring; or JS lib (Motion One, Framer Motion)

For Web, generate the linear(0, 0.1, 0.3, 0.6, 0.85, 1.0) form from the spring params at build time per preset; do NOT compute springs in JS at runtime.

R4.5 — Per-preset variation

PresetSpring behavior
material3Defaults (Expressive baseline)
material_expressive+20% stiffness on Spatial (snappier)
cyberpunk_neon-30% stiffness on Spatial (more "weighty" feel)
brutalist / terminal_classicSprings replaced by 0ms snap
glassmorphismDamping ratio +10% (gentler settle)
  • motion.kmd — index over all Motion sub-specs
  • motion/easing-duration.kmd — duration tokens + easing curves
  • motion/transitions.kmd — transition pattern catalog
  • interaction/states.kmd — state-level motion details
  • themes/ui-style.kmd — per-preset motion variation

References