animate*AsState: Animate Values in Jetpack Compose

Quick answer: use
animate*AsStatewhen one UI value should smoothly follow state: aFloatfor scale or alpha, aColor, aDp, anOffset, or another supported type. Give it a target value and use the returned animated value in the UI. It handles target changes while it is running; useAnimatablewhen you need imperative control or cancellation, andupdateTransitionwhen several values must share one state transition.
animate*AsState is Compose’s simplest target-based animation API. The call site owns a remembered animation. Each new target causes the animation to head toward that target, and Compose supplies its current value to the UI on each frame. If the target changes mid-flight, it continues from the current value and velocity instead of jumping back to the start.
The mental model: state in, animated state out
An animate*AsState call returns State<T>, commonly read with Kotlin’s by delegate:
val scale by animateFloatAsState(
targetValue = if (selected) 1.04f else 1f,
label = "selected scale",
)selected is the source of truth. scale is a presentation value that changes over time on its way to the target. Do not make the animated value your business state; if the user taps a button, update the real state immediately and let the animation communicate that change.
This fits naturally with state hoisting: a parent or ViewModel owns selected, while a stateless composable turns that input into motion. The animation begins with the first target that reaches the call site, so it should stay in composition for the lifetime you want it to animate.
A complete single-value example
This save button animates its container color and a small scale change from one Boolean. Each property is independent, which is appropriate because neither needs to be coordinated as one named transition.
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
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
@Composable
fun SaveButton(
saved: Boolean,
onSavedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val containerColor by animateColorAsState(
targetValue = if (saved) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.secondaryContainer
},
animationSpec = tween(220),
label = "save container color",
)
val scale by animateFloatAsState(
targetValue = if (saved) 1.04f else 1f,
animationSpec = tween(
durationMillis = 180,
easing = FastOutSlowInEasing,
),
label = "save button scale",
)
FilledTonalButton(
onClick = { onSavedChange(!saved) },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = containerColor,
),
modifier = modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.padding(8.dp),
) {
Text(if (saved) "Saved" else "Save")
}
}The animation is not launched from the click handler. The click changes saved; recomposition gives both animation calls their new targets. This distinction keeps input, state, and rendering easy to test.
Choose the variant that matches the value
Compose supplies animate*AsState overloads for common UI types, including Float, Color, Dp, Size, Offset, Rect, Int, IntOffset, and IntSize. For a domain type of your own, use animateValueAsState with a TwoWayConverter. The official value-based animation guide lists the supported types and the custom-type route.
| UI value you need | Usual API | Example use |
|---|---|---|
| Opacity, scale, rotation, progress | animateFloatAsState | A pressed surface scales slightly |
| A Material or custom color | animateColorAsState | A selected filter changes its container color |
| Size or spacing in density-independent pixels | animateDpAsState | A card elevation or padding changes |
| Position | animateOffsetAsState or animateIntOffsetAsState | A small badge shifts with state |
| A type with a meaningful vector representation | animateValueAsState | A custom value converted with TwoWayConverter |
Keep the animated type aligned with the modifier or parameter that consumes it. For example, animate Dp when an API expects Dp; do not constantly convert an animated Float into pixels just to animate layout size.
Pick a spec for the interaction, not for decoration
The default is suitable for many state changes, but motion communicates a relationship. Pass an animationSpec when timing or feel is part of the UI contract.
val elevation by animateDpAsState(
targetValue = if (raised) 8.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium,
),
label = "card elevation",
)- Use
tweenwhen a fixed duration and easing curve match a brief, predictable response. - Use
springwhen an object should feel like it settles into a new value and interruptions should remain natural. - Use
keyframesonly when intermediate values are meaningful; it is not a substitute for a simpletween. - Use
snapwhen the value should change immediately. It can be useful as one branch of a larger transition, but it is not visual motion by itself.
Avoid treating duration as a brand token to paste everywhere. A press response, an expanding layout, and a repeated loading pulse have different jobs. Test the motion on a physical device, including reduced-scale and interrupted interactions.
Put frame-by-frame values in the right rendering layer
Animation state changes every frame. Where you consume that value matters. For a purely visual transform such as alpha, scale, or rotation, graphicsLayer can contain the invalidation to drawing:
val alpha by animateFloatAsState(
targetValue = if (enabled) 1f else 0.45f,
label = "enabled alpha",
)
Box(
Modifier.graphicsLayer {
this.alpha = alpha
},
)The animateFloatAsState API reference specifically notes that applying alpha in a graphics layer limits invalidation to drawing, unlike Modifier.alpha, which requires recomposition for each frame. That is not a reason to force every animation into a layer: if a value genuinely changes layout, animate the layout value and measure the result.
For a target derived from fast-changing input, first ask whether the visible result really needs every change. derivedStateOf explains how to collapse frequent input updates into a stable UI result when that distinction is real.
Know when animate*AsState is the wrong tool
| Need | Prefer | Why |
|---|---|---|
| Animate one value toward state | animate*AsState | Minimal, declarative target-based API |
| Stop, snap, or sequence an animation from a coroutine | Animatable | Explicit suspend operations and cancellation control |
| Animate several values from the same screen state | updateTransition | One transition owns the coordinated child values |
| Add or remove content | AnimatedVisibility | Content lifecycle and enter/exit motion are part of the requirement |
| Swap one content state for another | AnimatedContent | The composable manages content changes |
| Run a repeating decorative motion | rememberInfiniteTransition | Infinite values have no state-driven finishing target |
animateFloatAsState is deliberately fire-and-forget: the reference says it cannot be stopped without removing its composable from the tree. Do not try to simulate cancellation by inventing extra Boolean state. Move to Animatable when the user must be able to cancel, scrub, or sequence the motion.
Common mistakes
Wrapping the call in remember
Do not write remember { animateFloatAsState(...) }. It is itself a composable animation API that remembers its animation at the call site. Call it during composition and supply the latest target.
Starting an animation from a side effect for normal UI state
For a normal state-driven value, LaunchedEffect is unnecessary. Pass the target to animate*AsState. Reserve effects for work with a lifecycle or coroutine requirement; see LaunchedEffect: when and how to use it.
Putting important app work in finishedListener
The finish callback is a rendering hook, not a reliable business-event pipeline. A new target can redirect an in-flight animation. Persist data, navigate, and update application state from the user action or state owner; use animation callbacks only for local, optional visual follow-up.
Using independent animations for a single semantic state machine
Two animate*AsState calls are fine for a button’s color and scale. When several values must start, change spec, and finish as one transition between named states, model those states and use updateTransition instead.
Inspect and test the behavior
Give each call a useful label. Android Studio’s Animation Preview can inspect animate*AsState animations frame by frame and show their values, which is much faster than guessing at a duration.
For UI tests, test the state boundary first: tap the control, assert the semantic result such as “Saved,” then use a Compose test clock or screenshot test if intermediate motion is a required part of the experience. Also test a quick second tap: target-based animation should redirect cleanly rather than briefly returning to its original value.
FAQ
Does animate*AsState restart on every recomposition?
No. Recomposition supplies a target value, but the animation only needs new motion when that target changes. Recomposition during the animation is expected because the returned state value changes every frame.
Can it animate more than one property?
You can call it for several properties, but those animations are independently owned. Use updateTransition when their relationship is important enough to model as one transition.
Should I use it for an infinite pulse?
No. An infinite animation has no final target, so use rememberInfiniteTransition. animate*AsState is for values that should settle at a state-derived target.