AnimatedVisibility

animate*AsState animates a value that always exists. But sometimes a whole composable should exist one moment and be gone the next — an error message that only appears while a form is invalid, a "loading more" row at the bottom of a list. Simply wrapping that composable in an if makes it pop in and out instantly. AnimatedVisibility replaces that if with a container that keeps the composable around just long enough to animate its entrance and exit.

Basic Usage

AnimatedVisibility takes a boolean visible parameter and a content lambda, much like an if block would, except the content composable is kept alive during its exit animation even after visible flips to false:

The simplest form

AnimatedVisibility(visible = showBanner) {
    Text("Recipe saved!")
}

With no other arguments, Compose uses sensible default enter/exit transitions (a fade combined with an expand/shrink). Most of the time you'll want to be explicit about the motion, which is what the enter and exit parameters are for.

fadeIn/fadeOut, slideInVertically/slideOutVertically & expandVertically/shrinkVertically

Compose ships a small library of EnterTransition and ExitTransition builders that each describe one visual effect. The three most commonly reached for:

FunctionEffect
fadeIn() / fadeOut()Animates alpha from/to 0
slideInVertically() / slideOutVertically()Animates a vertical offset, controlled by an initialOffsetY/targetOffsetY lambda
expandVertically() / shrinkVertically()Animates the composable's own height from/to 0, pushing surrounding content out of the way as it grows

Each of these accepts its own animationSpec, exactly like animate*AsState did in the previous lesson:

One transition at a time

AnimatedVisibility(
    visible = showError,
    enter = slideInVertically(animationSpec = tween(200)),
    exit = slideOutVertically(animationSpec = tween(150))
) {
    Text("Something went wrong")
}

Combining Transitions with the + Operator

Real UI motion is rarely just one effect. EnterTransition and ExitTransition values can be combined with +, running every combined effect together:

Fade, slide and expand together

val enter = fadeIn(animationSpec = tween(200)) +
    slideInVertically(animationSpec = tween(200)) +
    expandVertically(animationSpec = tween(200))

Order doesn't matter for how the effects run — Compose runs every combined transition on the same clock — but keeping the same animationSpec duration across all of them avoids one effect visibly finishing before another.

Worked Example: RecipeBox's Title Validation Error

When saving a new recipe, RecipeBox validates that the title field isn't blank. Rather than the error text just appearing (or worse, always reserving space below the field even when there's nothing wrong), it slides and fades in when validation fails, and slides back out once the user fixes it:

RecipeBox: the title field with an animated error

@Composable
fun RecipeTitleField(
    title: String,
    onTitleChange: (String) -> Unit,
    showError: Boolean
) {
    Column {
        OutlinedTextField(
            value = title,
            onValueChange = onTitleChange,
            label = { Text("Recipe title") },
            isError = showError,
            modifier = Modifier.fillMaxWidth()
        )
        AnimatedVisibility(
            visible = showError,
            enter = fadeIn(animationSpec = tween(200)) +
                slideInVertically(
                    animationSpec = tween(200),
                    initialOffsetY = { fullHeight -> -fullHeight / 2 }
                ) +
                expandVertically(animationSpec = tween(200)),
            exit = fadeOut(animationSpec = tween(150)) +
                slideOutVertically(
                    animationSpec = tween(150),
                    targetOffsetY = { fullHeight -> -fullHeight / 2 }
                ) +
                shrinkVertically(animationSpec = tween(150))
        ) {
            Text(
                text = "Title can't be empty",
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall,
                modifier = Modifier.padding(top = 4.dp)
            )
        }
    }
}

The negative initialOffsetY/targetOffsetY makes the message slide in and out from slightly above its resting position rather than from off-screen, which reads as "unfolding from the field above it" instead of an unrelated element sliding up from below.

AnimatedVisibility vs. a Plain if

Note: A plain if (visible) { Content() } removes the composable from composition the instant visible becomes false — there's no exit animation to play because there's nothing left to animate. AnimatedVisibility keeps Content() composed for the duration of the exit transition and only removes it once that transition finishes.
Next up: AnimatedVisibility answers "is this composable here or not?" — Crossfade & AnimatedContent covers animating between two or more different composables entirely.