updateTransition: Animate Multiple Values Together

Quick answer: use updateTransition when 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 child animate* 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 valuePossible specWhy
Container colorShort tweenA color can settle promptly
Corner radiusMedium springShape can feel responsive without bounce
ElevationStiff springA lifted surface can settle quickly
Progress valuetween with a defined durationProgress 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

NeedPreferWhy
One value follows stateanimate*AsStateLess ceremony for a single target-based value
Several values express one semantic stateupdateTransitionShared target state and inspectable child animations
Content appears or is removedAnimatedVisibilityControls content lifecycle and enter/exit work
Content is replaced by another stateAnimatedContent or CrossfadeControls outgoing and incoming content trees
Gesture or coroutine must stop, snap, or sequenceAnimatableImperative 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.