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
| Approach | When to use it |
|---|---|
Several independent animate*AsState calls | Properties 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 |
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."