updateTransition: Animate Multiple Values Together

Quick answer: use
updateTransitionwhen several values belong to the same UI state change—for example, an expanded card’s color, corner radius, elevation, and scale. Give the transition a meaningful target state, then define childanimate*values from that state. The children move toward the same target together, while each can still use its own animation spec for a particular state pair.
The official value-based animation guide describes Transition as the owner of one or more child animations that run between states. updateTransition creates and remembers that transition at the call site, then updates its target as state changes. It is the declarative choice for a state machine with several visual consequences.
Use a semantic state, not a collection of animation flags
This is a good state model:
enum class DetailsCardState {
Compact,
Expanded,
}It expresses what the UI is. A single DetailsCardState can determine several visual values. Compare that with separate isLarge, isRaised, isHighlighted, and isRound flags: invalid combinations become possible and it is hard to tell which transition should happen when they change together.
State still belongs with the screen or component that owns the interaction. A reusable card receives the target state and sends an event upward; state hoisting covers that boundary. updateTransition only turns the already-correct state into motion.
A complete coordinated transition
This example makes a card grow, soften its shape, lift, and change color when its semantic state changes. All four values are children of one Transition.
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
enum class DetailsCardState { Compact, Expanded }
@Composable
fun DetailsCard(state: DetailsCardState) {
val transition = updateTransition(
targetState = state,
label = "details card",
)
val size by transition.animateDp(label = "card size") { target ->
if (target == DetailsCardState.Expanded) 176.dp else 120.dp
}
val cornerRadius by transition.animateDp(
transitionSpec = {
if (DetailsCardState.Compact isTransitioningTo DetailsCardState.Expanded) {
spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium,
)
} else {
tween(durationMillis = 180)
}
},
label = "card corner radius",
) { target ->
if (target == DetailsCardState.Expanded) 28.dp else 12.dp
}
val containerColor by transition.animateColor(label = "card color") { target ->
if (target == DetailsCardState.Expanded) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
}
}
val scale by transition.animateFloat(label = "card scale") { target ->
if (target == DetailsCardState.Expanded) 1.03f else 1f
}
Surface(
color = containerColor,
shape = RoundedCornerShape(cornerRadius),
modifier = Modifier
.size(size)
.graphicsLayer {
scaleX = scale
scaleY = scale
},
) {
Box(Modifier.padding(16.dp)) {
Text(if (state == DetailsCardState.Expanded) "More details" else "Details")
}
}
}The target parameter in each child mapping describes the state being reached. The current interpolated values are returned as Compose State objects and update frame by frame. A target change during an active transition redirects all child animations toward the new state; you do not restart them from a click handler or LaunchedEffect.
The updateTransition reference confirms that each target-state change runs child animations toward their values for the new target. It also notes that labels distinguish the transition in Android Studio.
“Together” means one state model, not identical timing
Child animations share the same transition state, but they do not need identical specs or durations. That is useful when the relationship is coordinated but not mechanically uniform:
| Child value | Possible spec | Why |
|---|---|---|
| Container color | Short tween | A color can settle promptly |
| Corner radius | Medium spring | Shape can feel responsive without bounce |
| Elevation | Stiff spring | A lifted surface can settle quickly |
| Progress value | tween with a defined duration | Progress may have a timing contract |
Use transitionSpec on a child when the motion should differ by direction. The example uses a spring while expanding and a shorter tween while collapsing. The Transition.Segment supplies initialState, targetState, and isTransitioningTo, so the spec can express that policy without adding more flags.
Do not give every property a different personality. One state change should still read as one interaction. Coordinating through a transition helps you centralize the relationships, but design judgment still determines whether the motion is understandable.
When updateTransition is the right level of API
| Need | Prefer | Why |
|---|---|---|
| One value follows state | animate*AsState | Less ceremony for a single target-based value |
| Several values express one semantic state | updateTransition | Shared target state and inspectable child animations |
| Content appears or is removed | AnimatedVisibility | Controls content lifecycle and enter/exit work |
| Content is replaced by another state | AnimatedContent or Crossfade | Controls outgoing and incoming content trees |
| Gesture or coroutine must stop, snap, or sequence | Animatable | Imperative suspension and cancellation control |
For a single color, alpha, or scale, animate*AsState remains clearer. Move to a transition when multiple values are truly facets of the same state—not merely because a composable happens to contain several animations.
For a state replacement such as loading → content → error, use AnimatedContent or Crossfade instead. A Transition animates values; content-replacement APIs manage the temporary overlap and disposal of different content trees.
Compose visibility into a parent transition
When an expanded card also reveals optional content, the visibility can be a child of the parent transition instead of an unrelated Boolean animation:
import androidx.compose.animation.AnimatedVisibility
transition.AnimatedVisibility(
visible = { target -> target == DetailsCardState.Expanded },
label = "expanded details",
) {
ExpandedDetails()
}This keeps the card’s value changes and the optional content’s lifecycle connected to the same state machine. The animation documentation describes child transitions and AnimatedVisibility as part of the broader transition system. For standalone show/hide behavior, use AnimatedVisibility directly.
Start-in and restart cases use a different API
updateTransition(targetState) starts from its first target state. Do not recreate it with a random key just to force an entrance animation. When you deliberately need an initial state different from the first target—or need to control/restart a transition—use the current rememberTransition APIs with an appropriate transition state.
The older updateTransition(MutableTransitionState) overload is deprecated. The current API reference directs that use case to rememberTransition instead. For ordinary state updates, keep updateTransition(targetState).
Common mistakes
Creating separate states for one visual state
If color, size, and elevation always change together, do not give each one its own independently toggled Boolean. Model one semantic state and derive each visual target from it.
Doing app work when an animation finishes
The transition is a rendering detail. Save data, navigate, and call business logic from events or a state holder, not from an animation completing. UI state, events, and one-time effects explains why those lifecycles should stay separate.
Replacing a transition with a large when of animate*AsState calls
Several separate animations can reach the same final look, but they obscure the relationship and make pair-specific specs harder to reason about. Use updateTransition when the transition itself is a coherent unit.
Changing target state too frequently
Transitions handle interruption, but that does not make every changing value a good target. Do not drive a multi-value state machine from scroll pixels or every text keystroke. First derive a stable visual state, then animate meaningful changes.
Inspect and test the state graph
Give the parent transition and each child a descriptive label. Android Studio’s Animation Preview can inspect updateTransition, its values, and transitions between target states.
In UI tests, assert the durable target state first. If intermediate visual timing is part of the requirement, use a controlled Compose test clock or screenshot test. Test every important edge in the state graph—compact → expanded, expanded → compact, and a rapid reversal—rather than testing only the happy direction.
FAQ
Does every child animation finish at the same time?
Not necessarily. They are coordinated by one transition state, but each child can use a different finite animation spec. Choose that difference deliberately.
Can a transition animate a custom type?
Yes. Use transition.animateValue with a TwoWayConverter when the type can be represented by an animation vector. Use the built-in typed animate* extensions for common values first.
Is updateTransition for navigation?
No. It is for UI values driven by a local or screen state. Navigation destinations have back-stack and route lifecycle concerns that belong in navigation transition APIs.