The Transition API

Sometimes one state change needs to drive several animated properties at once, all perfectly in sync — a selected card that should grow slightly, lift with more elevation, and gain a colored border, all starting and finishing together. You could call three separate animate*AsState functions off the same boolean, and it would mostly work. But when properties need to be coordinated as a single unit — sharing one label in the animation inspector, guaranteed to be driven by exactly the same state transition — Compose's updateTransition is the tool built for that.

updateTransition: One State, Many Properties

updateTransition takes a target state and a label, and returns a Transition object. On its own, a Transition doesn't animate anything — it's a coordinator that you then ask for one or more animated child values:

Creating the transition

val transition = updateTransition(targetState = isSelected, label = "recipeCardSelection")

isSelected here can be any type — a Boolean, an enum describing several distinct visual states, whatever your UI needs. Every property you derive from transition below reacts to changes in that same value, together.

transition.animateFloat{} and transition.animateColor{}

Once you have a Transition, you ask it for each animated property with animateFloat, animateColor, animateDp, and similar functions. Each one takes its own optional transitionSpec (so different properties can move at different speeds) and a lambda mapping the transition's state to that property's target value:

Two properties, one shared transition

val scale by transition.animateFloat(
    label = "cardScale",
    transitionSpec = { spring(dampingRatio = Spring.DampingRatioMediumBouncy) }
) { selected -> if (selected) 1.03f else 1f }

val elevation by transition.animateDp(
    label = "cardElevation",
    transitionSpec = { tween(durationMillis = 200) }
) { selected -> if (selected) 8.dp else 2.dp }

Both scale and elevation are driven by the exact same underlying isSelected flip; because they share the parent transition, Compose tracks them as one coordinated unit for tooling and for lifecycle purposes, even though each has its own timing curve.

Worked Example: RecipeBox's Selectable Recipe Card

RecipeBox's recipe list supports a multi-select mode for bulk actions (delete several recipes at once, add several to a shopping list). When a card is selected, it should scale up slightly, lift with more elevation, and gain a primary-colored border — three separate properties that all need to animate together off the same isSelected flag:

RecipeBox: a card that animates scale, elevation & border color together

@Composable
fun RecipeCard(
    recipe: Recipe,
    isSelected: Boolean,
    onClick: () -> Unit
) {
    val transition = updateTransition(targetState = isSelected, label = "recipeCardSelection")

    val scale by transition.animateFloat(
        label = "cardScale",
        transitionSpec = { spring(dampingRatio = Spring.DampingRatioMediumBouncy) }
    ) { selected -> if (selected) 1.03f else 1f }

    val elevation by transition.animateDp(
        label = "cardElevation",
        transitionSpec = { tween(durationMillis = 200) }
    ) { selected -> if (selected) 8.dp else 2.dp }

    val borderColor by transition.animateColor(
        label = "cardBorderColor",
        transitionSpec = { tween(durationMillis = 200) }
    ) { selected -> if (selected) MaterialTheme.colorScheme.primary else Color.Transparent }

    Card(
        modifier = Modifier
            .scale(scale)
            .border(width = 2.dp, color = borderColor, shape = MaterialTheme.shapes.medium)
            .clickable { onClick() },
        elevation = CardDefaults.cardElevation(defaultElevation = elevation)
    ) {
        RecipeCardContent(recipe)
    }
}

Tapping a card in multi-select mode flips isSelected, and all three properties animate out of the same transition — the spring-based scale gives the tap some bounce while the tween-based elevation and border color ease in more smoothly, and all three finish reacting to the exact same state flip rather than being three independently-triggered animations that could drift out of sync.

Transition vs. Separate animate*AsState Calls

ApproachWhen to use it
Several independent animate*AsState callsProperties are logically unrelated, or only one or two values are involved
updateTransition + transition.animate*{}Multiple properties conceptually belong to the same visual state change and should be grouped, inspected, and reasoned about together
Note: Functionally, an updateTransition with three child animations and three separate animate*AsState calls driven by the same boolean often produce visually identical results — the Transition API's real benefit is organizational: one named transition in the animation inspector instead of three, and one place that owns "what does selection state animate."
Next up: Every animation so far has been driven by a state change your own code decided on. Gesture-Driven Animations covers animations driven directly by the user's finger.