| name | kmp-animations |
| description | Animation APIs in Compose Multiplatform (animate*AsState, updateTransition, AnimatedContent, Animatable) and the performance caveats that differ between Android, iOS, and Desktop. Use when adding motion to shared UI. |
KMP Animations
Instructions
Compose Multiplatform ships the full Jetpack Compose animation API. Code is identical across targets; runtime cost is not.
1. Value-based animations
@Composable
fun LikeButton(isLiked: Boolean, onClick: () -> Unit) {
val scale by animateFloatAsState(
targetValue = if (isLiked) 1.2f else 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "like-scale",
)
IconButton(onClick = onClick, modifier = Modifier.graphicsLayer { scaleX = scale; scaleY = scale }) {
Icon(if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, contentDescription = null)
}
}
2. Transitions for multiple coordinated values
@Composable
fun Chip(selected: Boolean, text: String) {
val t = updateTransition(selected, label = "chip")
val bg by t.animateColor(label = "bg") { if (it) Color(0xFF6750A4) else Color(0xFFE7E0EC) }
val fg by t.animateColor(label = "fg") { if (it) Color.White else Color(0xFF1D1B20) }
Box(Modifier.background(bg, RoundedCornerShape(8.dp)).padding(horizontal = 12.dp, vertical = 6.dp)) {
Text(text, color = fg)
}
}
3. Enter/exit content
AnimatedContent(
targetState = screen,
transitionSpec = {
(slideInHorizontally { it } + fadeIn()) togetherWith
(slideOutHorizontally { -it } + fadeOut())
},
label = "screen-swap",
) { current ->
when (current) {
Screen.Home -> HomeUi()
Screen.Detail -> DetailUi()
}
}
4. Gesture-driven — Animatable
val offset = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Modifier.pointerInput(Unit) {
detectHorizontalDragGestures(
onHorizontalDrag = { _, dx ->
scope.launch { offset.snapTo(offset.value + dx) }
},
onDragEnd = {
scope.launch { offset.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow)) }
},
)
}.graphicsLayer { translationX = offset.value }
Animatable is the only animation API that supports cancellable physics-based animations driven by gestures — always prefer it over animateFloatAsState for drag-to-dismiss.
5. Cross-target performance notes
- iOS: Compose renders through Skia on Metal. Very large, frequently-changing lists combined with per-item animations can hit the frame budget harder than on Android. Keep item-level animations cheap (opacity / translation) and use
Modifier.graphicsLayer — it forces a separate layer that compositing can reuse.
- Desktop (JVM): animations render on a software / OpenGL canvas depending on the platform. On Linux without a compositor they can stutter; keep animations opt-in behind
LocalDensity.current.fontScale or a settings flag.
- wasmJs: every animation frame incurs a JS <-> wasm bridge cost. Prefer CSS-free Compose animations over mixing with HTML.
6. Avoid re-triggering animations
Wrap the changing parameter in remember { derivedStateOf { ... } } so only real changes drive animate*AsState. Do not call animateFloatAsState inside a forEach over a mutable list without stable keys — each item recomposition restarts the animation.
7. Honouring reduced motion
expect val reducedMotionEnabled: Flow<Boolean>
@Composable
fun motionDuration(default: Int): Int {
val reduced by reducedMotionEnabled.collectAsState(initial = false)
return if (reduced) 0 else default
}
On iOS, bridge to UIAccessibilityIsReduceMotionEnabled(); on Android to Settings.Global.ANIMATOR_DURATION_SCALE.
Checklist