Compose Animations: Transition, AnimatedVisibility & Motion
Create fluid, production-quality animations in Jetpack Compose
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Compose Animations is a comprehensive animation system built into Jetpack Compose that provides declarative, state-driven animation APIs. From simple value animations (animate*AsState) to complex choreographed transitions (updateTransition, MotionLayout), it handles enter/exit animations (AnimatedVisibility), content swaps (AnimatedContent), infinite loops (InfiniteTransition), and gesture-driven motion (Animatable), all designed to be interruptible, composable, and performant.
Real-world relevance
Every polished app uses animations extensively. WhatsApp uses AnimatedVisibility-style animations for message reactions appearing. Instagram uses gesture-driven animations for story swiping and double-tap hearts. Twitter/X uses AnimatedContent for the like counter incrementing. Spotify uses InfiniteTransition for its equalizer bars. Google Maps uses MotionLayout-style coordinated transitions for its bottom sheet expanding into full screen.
Key points
- animate*AsState Fundamentals — The simplest animation API. Functions like `animateDpAsState`, `animateColorAsState`, `animateFloatAsState`, and `animateIntAsState` animate between values automatically when the target changes. They return a State that recomposes smoothly. Customize with `animationSpec` parameter: `tween(durationMillis, easing)`, `spring(dampingRatio, stiffness)`, `keyframes {}`, or `snap()`.
- AnimatedVisibility — Wraps content that appears/disappears with enter/exit transitions. EnterTransition combinators: `fadeIn() + slideInVertically() + expandVertically()`. ExitTransition: `fadeOut() + slideOutHorizontally() + shrinkVertically()`. Use `MutableTransitionState(false)` for initial animation on first composition. Each child can override with `Modifier.animateEnterExit()` for individual control.
- AnimatedContent & ContentTransform — Animates between different composable content based on a target state. Uses `transitionSpec` returning `ContentTransform` created by combining `EnterTransition togetherWith ExitTransition`. The `using SizeTransform(clip = true)` controls how the container resizes. Use `targetState` in the content lambda for type-safe state-driven UI swaps like number counters or page transitions.
- updateTransition & Multi-property Animation — `updateTransition(targetState)` creates a Transition object that coordinates multiple animated properties simultaneously. Use `transition.animateDp {}`, `transition.animateColor {}`, etc. with labels for inspection. All child animations are synchronized — they start, update, and complete together. MutableTransitionState enables observing `isIdle`, `currentState`, and `targetState`.
- Crossfade & AnimatedNavHost — Crossfade is a simplified AnimatedContent that only does fade transitions between states. Ideal for tab switching or simple content swaps. For Navigation Compose, use `AnimatedNavHost` from accompanist (now integrated into navigation-compose) which provides `enterTransition`, `exitTransition`, `popEnterTransition`, and `popExitTransition` per route.
- InfiniteTransition — `rememberInfiniteTransition()` creates animations that loop forever — perfect for loading indicators, pulsing effects, or rotating icons. Use `infiniteTransition.animateFloat(initialValue, targetValue, animationSpec = infiniteRepeatable(tween(), RepeatMode.Reverse))`. RepeatMode.Restart snaps back; RepeatMode.Reverse ping-pongs smoothly.
- AnimationSpec Deep Dive — `spring()` is physics-based with DampingRatioNoBouncy/LowBouncy/MediumBouncy/HighBouncy and stiffness levels. `tween()` uses duration + easing (FastOutSlowInEasing, LinearEasing, etc.). `keyframes {}` defines values at specific times for complex curves. `repeatable()` wraps any spec with iteration count. `snap()` jumps instantly with optional delay.
- Gesture-Driven Animations — Combine `Modifier.pointerInput {}` or `Modifier.draggable {}` with `Animatable` for gesture-driven motion. `Animatable` supports `animateTo()`, `snapTo()`, and crucially `stop()` for interrupting animations mid-flight. Use `animateDecay()` with `splineBasedDecay()` for fling physics. `AnchoredDraggable` (replacing `SwipeableState`) handles swipe-to-dismiss and bottom sheet snapping.
- Modifier.graphicsLayer for Performance — Use `Modifier.graphicsLayer { }` for GPU-accelerated transforms: `translationX/Y`, `scaleX/Y`, `rotationZ`, `alpha`. These don't trigger recomposition or relayout — only redraw. Combine with `animateFloatAsState` for smooth, performant animations. For shared element transitions, `Modifier.sharedElement()` in Navigation Compose handles cross-screen hero animations.
- MotionLayout in Compose — MotionLayout from ConstraintLayout Compose enables complex, coordinated animations between two constraint sets. Define `start` and `end` ConstraintSets, animate `progress` from 0f to 1f. Supports custom properties, arcs, stagger, and keyframes. Ideal for collapsing toolbars, coordinated reveal animations, and complex layout transitions that would be impractical with standard Compose animation APIs.
- Animation Testing & Inspection — Use `createComposeRule()` with `mainClock.autoAdvance = false` to control animation timing. `advanceTimeBy(millis)` steps through animations frame by frame. Compose Animation Preview in Android Studio visualizes transitions. Add `label` parameters to animated values for identification in the Animation Inspector. Test final states with `waitForIdle()` after advancing time.
- Performance Best Practices — Defer reads with `Modifier.graphicsLayer { }` lambda to skip recomposition. Use `derivedStateOf {}` to prevent unnecessary recompositions from rapid animation value changes. Prefer `Animatable` over `animate*AsState` when you need interruption handling. Profile with Layout Inspector's recomposition counter — animations should only trigger redraws, not recompositions.
Code example
// === animate*AsState basics ===
@Composable
fun ExpandableCard(title: String, content: String) {
var expanded by remember { mutableStateOf(false) }
val elevation by animateDpAsState(
targetValue = if (expanded) 8.dp else 2.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "cardElevation"
)
val backgroundColor by animateColorAsState(
targetValue = if (expanded)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface,
animationSpec = tween(300),
label = "cardColor"
)
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded },
elevation = CardDefaults.cardElevation(elevation),
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
AnimatedVisibility(
visible = expanded,
enter = fadeIn(tween(300)) +
expandVertically(
animationSpec = spring(
stiffness = Spring.StiffnessLow
)
),
exit = fadeOut(tween(200)) +
shrinkVertically()
) {
Text(
text = content,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
}
// === AnimatedContent with number counter ===
@Composable
fun AnimatedCounter(count: Int) {
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
slideInVertically { height -> height } +
fadeIn() togetherWith
slideOutVertically { height -> -height } +
fadeOut()
} else {
slideInVertically { height -> -height } +
fadeIn() togetherWith
slideOutVertically { height -> height } +
fadeOut()
}.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayLarge
)
}
}
// === updateTransition for coordinated animation ===
enum class BoxState { Collapsed, Expanded }
@Composable
fun TransitionBox() {
var currentState by remember {
mutableStateOf(BoxState.Collapsed)
}
val transition = updateTransition(
targetState = currentState,
label = "boxTransition"
)
val size by transition.animateDp(
transitionSpec = {
if (targetState == BoxState.Expanded) {
spring(stiffness = Spring.StiffnessLow)
} else {
spring(stiffness = Spring.StiffnessMedium)
}
},
label = "size"
) { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 200.dp
}
}
val cornerRadius by transition.animateDp(
label = "corner"
) { state ->
when (state) {
BoxState.Collapsed -> 32.dp
BoxState.Expanded -> 16.dp
}
}
val color by transition.animateColor(
label = "color"
) { state ->
when (state) {
BoxState.Collapsed -> Color(0xFF6200EE)
BoxState.Expanded -> Color(0xFF03DAC5)
}
}
Box(
modifier = Modifier
.size(size)
.clip(RoundedCornerShape(cornerRadius))
.background(color)
.clickable {
currentState = if (currentState == BoxState.Collapsed)
BoxState.Expanded else BoxState.Collapsed
}
)
}
// === InfiniteTransition for loading ===
@Composable
fun PulsingDot() {
val infiniteTransition = rememberInfiniteTransition(
label = "pulse"
)
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
val alpha by infiniteTransition.animateFloat(
initialValue = 0.4f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = Modifier
.size(24.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
)
}
// === Gesture-driven animation with Animatable ===
@Composable
fun SwipeToDismissCard(
onDismiss: () -> Unit,
content: @Composable () -> Unit
) {
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.offset {
IntOffset(offsetX.value.roundToInt(), 0)
}
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
scope.launch {
if (abs(offsetX.value) > size.width / 3) {
val target = if (offsetX.value > 0)
size.width.toFloat()
else
-size.width.toFloat()
offsetX.animateTo(
targetValue = target,
animationSpec = tween(300)
)
onDismiss()
} else {
offsetX.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring
.DampingRatioMediumBouncy
)
)
}
}
}
) { _, dragAmount ->
scope.launch {
offsetX.snapTo(
offsetX.value + dragAmount
)
}
}
}
) {
content()
}
}Line-by-line walkthrough
- 1. ExpandableCard uses animateDpAsState for elevation and animateColorAsState for background — both animate automatically when 'expanded' state changes.
- 2. The spring spec with DampingRatioMediumBouncy creates a playful bounce effect on the elevation change.
- 3. AnimatedVisibility wraps the content text — fadeIn + expandVertically creates a smooth reveal from top.
- 4. AnimatedCounter uses AnimatedContent where the transitionSpec checks direction (up vs down) to slide numbers appropriately.
- 5. slideInVertically { height -> height } slides in from the bottom (positive = below viewport), while { -height } slides from top.
- 6. togetherWith combines enter and exit into a ContentTransform. SizeTransform(clip = false) allows content to overflow during transition.
- 7. TransitionBox uses updateTransition to coordinate size, cornerRadius, and color — all animate in sync when BoxState changes.
- 8. Each transition.animate* call can have its own transitionSpec, allowing different physics per property while staying synchronized.
- 9. PulsingDot uses rememberInfiniteTransition with RepeatMode.Reverse for smooth scale and alpha oscillation.
- 10. graphicsLayer is used for scale and alpha — these are render-only changes that don't trigger recomposition.
- 11. SwipeToDismissCard uses Animatable for offsetX, enabling snapTo() during drag and animateTo() on release.
- 12. The drag threshold (size.width / 3) determines whether to dismiss or spring back — spring creates a natural bounce-back feel.
Spot the bug
@Composable
fun FadeToggle(visible: Boolean, content: @Composable () -> Unit) {
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(500),
label = "alpha"
)
Box(modifier = Modifier.alpha(alpha)) {
content()
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Compose Animation Official Guide (Android Developers)
- Animating Elements in Jetpack Compose (Google Codelabs)
- Compose Animation Deep Dive (Android Developers YouTube)
- A Visual Guide to Compose Animations (ProAndroidDev)