Animation modifiers and composables

Compose comes with built-in composables and modifiers for handling common animation use cases.

Built-in animated composables

Animate appearance and disappearance with AnimatedVisibility

Green composable showing and hiding itself
Figure 1. Animating the appearance and disappearance of an item in a column

The AnimatedVisibility composable animates the appearance and disappearance of its content.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

By default, the content appears by fading in and expanding, and it disappears by fading out and shrinking. The transition can be customized by specifying EnterTransition and ExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}

As you can see in the example above, you can combine multiple EnterTransition or ExitTransition objects with a + operator, and each accepts optional parameters to customize its behavior. See the references for more information.

EnterTransition and ExitTransition examples

EnterTransition ExitTransition
fadeIn
fade in animation
fadeOut
fade out animation
slideIn
slide in animation
slideOut
slide out animation
slideInHorizontally
slide in horizontally animation
slideOutHorizontally
slide out horizontally animation
slideInVertically
slide in vertically animation
slideOutVertically
slide out vertically animation
scaleIn
scale in animation
scaleOut
scale out animation
expandIn
expand in animation
shrinkOut
shrink out animation
expandHorizontally
expand horizontally animation
shrinkHorizontally
shrink horizontally animation
expandVertically
expand vertically animation
shrinkVertically
shrink vertically animation

AnimatedVisibility also offers a variant that takes a MutableTransitionState. This allows you to trigger an animation as soon as the AnimatedVisibility is added to the composition tree. It is also useful for observing the animation state.

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

Animate enter and exit for children

Content within AnimatedVisibility (direct or indirect children) can use the animateEnterExit modifier to specify different animation behavior for each of them. The visual effect for each of these children is a combination of the animations specified at the AnimatedVisibility composable and the child's own enter and exit animations.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

In some cases, you may want to have AnimatedVisibility apply no animations at all so that children can each have their own distinct animations by animateEnterExit. To achieve this, specify EnterTransition.None and ExitTransition.None at the AnimatedVisibility composable.

Add custom animation

If you want to add custom animation effects beyond the built-in enter and exit animations, access the underlying Transition instance via the transition property inside the content lambda for AnimatedVisibility. Any animation states added to the Transition instance will run simultaneously with the enter and exit animations of AnimatedVisibility. AnimatedVisibility waits until all animations in the Transition have finished before removing its content. For exit animations created independent of Transition (such as using animate*AsState), AnimatedVisibility would not be able to account for them, and therefore may remove the content composable before they finish.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(modifier = Modifier.size(128.dp).background(background))
}

See updateTransition for the details about Transition.

Animate based on target state with AnimatedContent

The AnimatedContent composable animates its content as it changes based on a target state.

Row {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(targetState = count) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

Note that you should always use the lambda parameter and reflect it to the content. The API uses this value as the key to identify the content that's currently shown.

By default, the initial content fades out and then the target content fades in (this behavior is called fade through). You can customize this animation behavior by specifying a ContentTransform object to the transitionSpec parameter. You can create ContentTransform by combining an EnterTransition with an ExitTransition using the with infix function. You can apply SizeTransform to the ContentTransform by attaching it with the using infix function.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() with
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() with
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition defines how the target content should appear, and ExitTransition defines how the initial content should disappear. In addition to all of the EnterTransition and ExitTransition functions available for AnimatedVisibility, AnimatedContent offers slideIntoContainer and slideOutOfContainer. These are convenient alternatives to slideInHorizontally/Vertically and slideOutHorizontally/Vertically that calculate the slide distance based on the sizes of the initial content and the target content of the AnimatedContent content.

SizeTransform defines how the size should animate between the initial and the target contents. You have access to both the initial size and the target size when you are creating the animation. SizeTransform also controls whether the content should be clipped to the component size during animations.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) with
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Animate child enter and exit transitions

Just like AnimatedVisibility, the animateEnterExit modifier is available inside the content lambda of AnimatedContent. Use this to apply EnterAnimation and ExitAnimation to each of the direct or indirect children separately.

Add custom animation

Just like AnimatedVisibility, the transition field is available inside the content lambda of AnimatedContent. Use this to create a custom animation effect that runs simultaneously with the AnimatedContent transition. See updateTransition for the details.

Animate between two layouts with Crossfade

Crossfade animates between two layouts with a crossfade animation. By toggling the value passed to the current parameter, the content is switched with a crossfade animation.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

Built-in animation modifiers

Animate composable size changes with animateContentSize

Green composable animating its size change smoothly.
Figure 2. Composable smoothly animating between a small and a larger size

The animateContentSize modifier animates a size change.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

List item animations

If you are looking to animate item reorderings inside a Lazy list or grid, take a look at the Lazy layout item animation documentation.