Skip to content

Motion

CortenaUI's motion language. A small, opinionated set of spring presets, duration tiers, and easing curves shared by every interactive component. Read at runtime through LocalMotion.current.

Concept

Motion is a design token, just like color or spacing. Components do not invent their own animation specs — they pick a preset that matches the role of the motion. This keeps the feel of the library coherent across components and across the surface as a whole.

The system distinguishes two kinds of motion:

  • Spring presets are the default for any motion that originates from user input or follows physical causality — press response, content shift, panel reveal. Spring fits because the user feels the physics.
  • Tween durations + easing are reserved for transitions that must be deterministic — alpha fade, color crossfade, indicator hide. Tween fits because the timing is what matters, not the physics.

Components must read both through LocalMotion.current and never call spring(...) or tween(...) with hardcoded numbers. The single documented exception is raw gesture-physics primitives (DampedAnimation, InteractiveHighlight), which need bespoke per-track specs to feel right under continuous pointer input.

API Reference

@Immutable
class Motion {
    val snappy: SpringSpec<Float>
    val smooth: SpringSpec<Float>
    val gentle: SpringSpec<Float>

    val fast: Int     // 150 ms
    val medium: Int   // 250 ms
    val slow: Int     // 450 ms

    val standardEasing: Easing
    val emphasizedEasing: Easing
    val linearEasing: Easing
}

val DefaultMotion: Motion
val LocalMotion: ProvidableCompositionLocal<Motion>

Spring Presets

Preset Stiffness Damping ratio Use case
snappy Spring.StiffnessHigh No bouncy Tight UI feedback (press, toggle thumb, indicator drag)
smooth Spring.StiffnessMediumLow No bouncy General content shift, panel reveal, item move
gentle Spring.StiffnessLow No bouncy Large overlays, dialog enter / exit, page transition

All three default to DampingRatioNoBouncy. Components that want a tactile overshoot — for example a button releasing from a press — should construct a custom spring() from these stiffness values plus a higher damping ratio (DampingRatioMediumBouncy) rather than redefining the stiffness.

Duration Tokens

Token Value Use case
fast 150 Tap feedback, small fades, color shifts
medium 250 Standard fades, indicator hide, content crossfade
slow 450 Emphasis transitions, dialog scrim

Durations are exposed as Int (ms) so they pass directly into tween().

Easings

Easing Curve Use case
standardEasing (0.4, 0.0, 0.2, 1.0) Default for most fades
emphasizedEasing (0.2, 0.0, 0.0, 1.0) Decelerated arrivals
linearEasing linear Color / alpha that must be even

Examples

Spring-driven content motion:

val motion = LocalMotion.current
val offset by animateDpAsState(targetOffset, animationSpec = motion.smooth)

Tap feedback that should feel instant:

val motion = LocalMotion.current
val scale by animateFloatAsState(if (pressed) 0.96f else 1f, animationSpec = motion.snappy)

Deterministic alpha fade:

val motion = LocalMotion.current
val alpha = remember { Animatable(0f) }
LaunchedEffect(visible) {
    alpha.animateTo(
        targetValue = if (visible) 1f else 0f,
        animationSpec = tween(motion.medium, easing = motion.standardEasing),
    )
}

Custom tactile overshoot — the documented pattern for exceptions like indicator drag-thicken:

val motion = LocalMotion.current
val scaleSpring =
    spring<Float>(
        stiffness = motion.smooth.stiffness,
        dampingRatio = Spring.DampingRatioMediumBouncy,
    )

Override

Provide a custom Motion to Theme() if you want to tune the language for a specific surface — for example a "reduce motion" accessibility mode that swaps every spring for a faster, less bouncy variant:

Theme(motion = MyReducedMotion) {
    // ...
}