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.
Review procedure
Identify the visual job: show/hide, one value, coordinated values, content swap, size change, or gesture-driven motion.
Choose the smallest API from the table below.
Check lifecycle semantics: should hidden content leave composition, keep focus/state, or only become transparent?
Check identity: render each AnimatedContent branch from its content-lambda target, then choose contentKey by visual shape rather than payload churn.
Check performance: keep frame-rate animation values as State and read them in layout/draw block modifiers when possible.
Escalate to Animatable or lower-level APIs only when target-state animation cannot express the motion.
Finish when the chosen API matches the visual and lifecycle needs, any content-swap identity is preserved, no simpler API fits, and the relevant behavior has been verified.
Pick the smallest animation API
Need
API
Show or hide a subtree with enter/exit semantics; content is removed after exit completes
animateFloatAsState 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 AnimatedVisibility (or conditional composition with AnimatedVisibility / AnimatedContent patterns from the quick guide).
Background color
Use animateColorAsState for smooth color targets.
For animated fills behind children, the quick guide recommends drawing with Modifier.drawBehind rather than Modifier.background() so the animated color is applied in the draw phase appropriately for performance.
Modifier.animateContentSize() animates layout size changes—common for expanding/collapsing text or dynamic chips—without hand-rolling width/height animations.
Value-based animations (animate*AsState)
Compose provides animate*AsState for Float, Dp, Color, Size, Offset, Rect, Int, IntOffset, IntSize, and more. You supply the target; the API owns the animation state.
Pass an AnimationSpec via animationSpec (e.g. spring, tween) when defaults are wrong for the UI.
Set a distinct label for debugging and tooling when multiple animations exist in one composable.
val width by animateDpAsState( targetValue = if (expanded) 200.dp else 56.dp, animationSpec = spring(dampingRatio = 0.7f, stiffness = Spring.StiffnessMedium), label = "fabWidth",)
Multiple properties: rememberTransition
When one piece of state (e.g. enum class Phase { A, B, C }) should drive several animated values in lockstep, use rememberTransition 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 animate*AsState calls that should stay visually synchronized but can drift if specs or targets diverge. Older code may use updateTransition; prefer rememberTransition for new code.
Choosing between content-level APIs
Use the official Choose an animation API tree when the table is not enough. Compressed rules:
Situation
Prefer
Same composable, different target values for layout properties
animate*AsState or rememberTransition
Different composable content for the same region (tabs, steps)
AnimatedContent (custom transitionSpec, contentKey) or simpler Crossfade
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 AnimatedContent on top of the same destination swap
Art-based motion (illustrations, Lottie, complex vector timelines) is outside this skill; use dedicated libraries.
Decision flow (high level)
AnimatedContent keys for state holders
AnimatedContent can keep outgoing and incoming content composed at the same time. Render from the content lambda's target value, not a captured outer state value; otherwise both branches can show the latest state and effects inside them can act on the wrong content identity.
kotlin
// Wrong: outgoing and incoming branches both read the latest selectedId.AnimatedContent(targetState = selectedId) { Destination(selectedId)}// Right: each branch keeps the identity AnimatedContent assigned to it.AnimatedContent(targetState = selectedId) { targetId -> Destination(targetId)}
When AnimatedContent receives a state-holder wrapper such as AsyncResult<T>, Result<T>, or a sealed UiState, 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 contentKey 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 contentKey, every unequal Success(value) 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 contentKey
Loading → Success → Error
Branch key: "loading", "content", "error"
Success item A → Success item B should crossfade
Stable item id
Success data refresh should update in place
Constant content key for Success
Error message text changes but error UI shape stays
Constant content key for Error
Animated values and composition performance
animate*AsState returns State that updates frequently. If that value feeds Modifier.offset, Modifier.graphicsLayer, scroll-adjacent layout, or other frame-rate paths, avoid reading it in the composable body with by and then passing it into value-form modifiers—use deferred reads (block modifiers, draw/ layout lambdas) instead. See Compose performance.
If recomposition counters spike during motion unrelated to bad stability, see Compose performance.
Fade with animateFloatAsState(alpha) but expect children to unmount
Use AnimatedVisibility or remove the subtree from composition when hidden
Three animateDpAsState calls that must stay in sync with one enum
One rememberTransition + child animations
Animated color on Modifier.background causing extra work
Prefer drawBehind { drawRect(animatedColor) } per quick guide
Chaining LaunchedEffect + manual Animatable for simple target animation
Prefer animate*AsState or rememberTransition unless gestures require Animatable
Ignoring Navigation’s own transitions
Use Nav APIs for destination transitions; do not duplicate with AnimatedContent for the same swap
Reading outer state inside AnimatedContent's content lambda
Render from the lambda target so outgoing and incoming content retain distinct identities
AnimatedContent(targetState = asyncResult) animates on every data refresh
Add contentKey based on the visual shape or stable item identity
RED/GREEN agent scenarios
Novel case: focus moves while AnimatedContent swaps between two destinations. RED renders both branches from captured outer state. GREEN renders and keys effects from the lambda target, then tests focus after the transition settles.
Counterexample: a single composable only animates one color value. GREEN keeps animateColorAsState and does not introduce AnimatedContent or content identity machinery.