Compose: animations
Core principle
Pick the smallest API that matches the problem: built-in visibility and layout transitions first, then a single animated value, then a shared transition object when several values must move together, then gesture-level or imperative APIs when the framework cannot express the motion.
Pick the smallest animation API
| Need | API |
|---|
| Show or hide a subtree with enter/exit semantics; content is removed after exit completes | |
| Animate one property toward a target derived from state | / / / / … |
| Several animated values keyed off one boolean, enum, or sealed state | + transition child animations (, , , , …) |
| Smooth size when child layout height/width changes (e.g. text wraps) | Modifier.animateContentSize()
|
| Swap between different composable trees for the same slot | or |
| User-driven motion (drag, fling, interruptible springs) | and related coroutine APIs (see Advanced pointers) |
Appear and disappear
Prefer when the UI should leave or join the tree with enter/exit transitions.
kotlin
AnimatedVisibility(visible = expanded) {
Text("Details…")
}
on alpha only fades; the composable
stays in composition and continues to participate in layout unless you gate it yourself. Use that tradeoff when you intentionally keep children mounted (state, focus) but visually hidden. For true remove-from-tree behavior, use
(or conditional composition with
/
patterns from the
quick guide).
Background color
Use
for smooth color targets.
For animated fills behind children, the
quick guide recommends drawing with
rather than
so the animated color is applied in the draw phase appropriately for performance.
kotlin
val background = animateColorAsState(
targetValue = if (selected) selectedColor else idleColor,
label = "background",
)
Box(
Modifier.drawBehind { drawRect(background.value) },
) { /* content */ }
Size changes
Modifier.animateContentSize()
animates layout size changes—common for expanding/collapsing text or dynamic chips—without hand-rolling width/height animations.
Value-based animations ()
Compose provides
for
,
,
,
,
,
,
,
,
, and more. You supply the
target; the API owns the animation state.
- Pass an via (e.g. , ) when defaults are wrong for the UI.
- Set a distinct for debugging and tooling when multiple animations exist in one composable.
- For completion or sequencing details, see Value-based animations.
kotlin
val width by animateDpAsState(
targetValue = if (expanded) 200.dp else 56.dp,
animationSpec = spring(dampingRatio = 0.7f, stiffness = Spring.StiffnessMedium),
label = "fabWidth",
)
Multiple properties:
When one piece of state (e.g.
enum class Phase { A, B, C }
) should drive
several animated values in lockstep, use
and define child animations on that transition:
kotlin
val transition = rememberTransition(targetState = phase, label = "phase")
val alpha by transition.animateFloat(label = "alpha") { target ->
if (target == Phase.Visible) 1f else 0f
}
val offset by transition.animateDp(label = "offset") { target ->
if (target == Phase.Visible) 0.dp else 24.dp
}
Avoid multiple independent
calls that should stay visually synchronized but can drift if specs or targets diverge. Older code may use
; prefer
for new code.
Choosing between content-level APIs
Use the official
Choose an animation API tree when unsure. Compressed rules:
| Situation | Prefer |
|---|
| Same composable, different target values for layout properties | or |
| Different composable content for the same region (tabs, steps) | (custom , ) or simpler |
| Pager-like swipe between pages | Horizontal pager APIs from the animation docs / Material—follow the choose-api guidance |
| Transitions owned by Navigation Compose | Use navigation’s built-in transitions rather than bolting on top of the same destination swap |
Art-based motion (illustrations, Lottie, complex vector timelines) is outside this skill; use dedicated libraries and the “additional resources” links on
Animations.
Decision flow (high level)
mermaid
flowchart TD
start[Animation_need]
start --> showHide{Show_or_hide_subtree}
showHide -->|yes| av[AnimatedVisibility]
showHide -->|no| oneProp{Single_property_to_target}
oneProp -->|yes| asState["animate*AsState"]
oneProp -->|no| multiProp{Many_props_one_state}
multiProp -->|yes| rt[rememberTransition]
multiProp -->|no| swapTree{Different_composable_content}
swapTree -->|yes| ac[AnimatedContent_or_Crossfade]
swapTree -->|no| advanced[Animatable_or_lower_level]
AnimatedContent keys for state holders
When
receives a state-holder wrapper such as
,
, or a sealed
, decide what should actually trigger the transition. Usually the animation should run when the
content shape changes (loading → content → error), not when the payload inside the same shape changes.
Use
to map rich state to the animation identity:
kotlin
AnimatedContent(
targetState = result,
contentKey = { state ->
when (state) {
AsyncResult.Loading -> "loading"
is AsyncResult.Success -> "content"
is AsyncResult.Error -> "error"
}
},
label = "profile-content",
) { state ->
when (state) {
AsyncResult.Loading -> Loading()
is AsyncResult.Success -> Profile(state.value)
is AsyncResult.Error -> ErrorMessage(state.throwable)
}
}
Without
, every unequal
can be treated as new content. That is useful if a payload change should animate, but noisy when fresh data updates the same screen shape.
Choose keys by visual shape:
| State change | Typical |
|---|
| Loading → Success → Error | Branch key: , , |
| Success item A → Success item B should crossfade | Stable item id |
| Success data refresh should update in place | Constant content key for |
| Error message text changes but error UI shape stays | Constant content key for |
Animated values and composition performance
returns
that updates frequently. If that value feeds
,
, scroll-adjacent layout, or other
frame-rate paths, avoid reading it in the composable body with
and then passing it into value-form modifiers—use
deferred reads (block modifiers, draw/ layout lambdas) instead. See
compose-state-deferred-reads
.
If recomposition counters spike during motion unrelated to bad stability, see
compose-recomposition-performance
.
Advanced pointers (read the linked docs)
- Gesture-driven or cancelable motion: with , decay, and —Advanced animation example: Gestures and Drag, swipe, and fling.
- Infinite or repeating cycles:
rememberInfiniteTransition
.
- Seekable / test-controlled progress: and related APIs for predictable timelines in tests or tooling.
Common mistakes
| Mistake | Fix |
|---|
Fade with animateFloatAsState(alpha)
but expect children to unmount | Use or remove the subtree from composition when hidden |
| Three calls that must stay in sync with one enum | One + child animations |
| Animated color on causing extra work | Prefer drawBehind { drawRect(animatedColor) }
per quick guide |
| Chaining + manual for simple target animation | Prefer or unless gestures require |
| Ignoring Navigation’s own transitions | Use Nav APIs for destination transitions; do not duplicate with for the same swap |
AnimatedContent(targetState = asyncResult)
animates on every data refresh | Add based on the visual shape or stable item identity |
When not to use this skill
- Side-effect timing (, clicks launching work): use .
- Deep performance tuning of where snapshot state is read: use
compose-state-deferred-reads
as the primary reference.