AnimatedVisibility in Jetpack Compose

Quick answer: use
AnimatedVisibilitywhen content should animate into or out of the composition. Change itsvisiblestate instead of conditionally removing the composable yourself. It runs the enter or exit transition, then removes hidden content after its exit work finishes. Use it for a details panel, inline error, scroll-to-top button, or other content whose presence changes—not merely for a visual alpha change.
The official Compose animation guide describes AnimatedVisibility as the API for appearance and disappearance. Its lifecycle behavior is the important part: hidden content is eventually removed from composition, whereas a box with animated alpha remains laid out and can still matter to accessibility services.
Keep the host in composition; change visible
AnimatedVisibility needs to stay in the tree long enough to run the exit transition. Put the condition in visible, not around the composable:
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun FilterSummary(
hasActiveFilters: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = hasActiveFilters,
enter = fadeIn(animationSpec = tween(150)) +
expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
modifier = modifier,
label = "filter summary",
) {
Card(Modifier.fillMaxWidth()) {
Text(
text = "Filters are active",
modifier = Modifier.padding(16.dp),
)
}
}
}When hasActiveFilters changes to false, Compose keeps the card around for its exit animation, then removes it. The default transitions already fade and expand content in, then fade and shrink it out; provide explicit transitions only when they reinforce the spatial meaning of the UI.
This is different from the tempting but broken pattern below:
// Wrong: when false, AnimatedVisibility leaves composition immediately.
if (hasActiveFilters) {
AnimatedVisibility(visible = true) {
FilterSummaryContent()
}
}There is no host left to animate the exit. Keep a stable AnimatedVisibility call and update its target state instead.
Choose transitions that explain the change
EnterTransition and ExitTransition can be combined with +. A panel that pushes nearby content down can use vertical expand/shrink. A floating control that is only temporarily relevant may be clearer with a fade plus a short slide. Avoid adding every effect at once: an element should have one understandable source and destination.
| UI change | Good starting transition | Why |
|---|---|---|
| Inline validation or filter details | expandVertically / shrinkVertically | The layout gains or loses vertical content |
| Temporary floating affordance | fadeIn + slideInVertically | It enters from a nearby edge without claiming permanent space |
| Small optional label | fadeIn / fadeOut | Motion should stay quiet when geometry does not change |
| A compact chip or card | scaleIn / scaleOut plus fade | A local emphasis change without a large layout movement |
Use duration and easing as part of the interaction contract, not as decoration. The next decision is whether the element is appearing or disappearing (AnimatedVisibility) or whether one piece of content is being replaced by another (AnimatedContent). Those are different lifecycle problems.
Parent and child motion can work together
Children inside the content lambda can use Modifier.animateEnterExit. Their transition is combined with the transition configured on the parent, so the container can fade while a child action row slides.
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateEnterExit
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun RetryMessage(visible: Boolean, onRetry: () -> Unit) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut(),
label = "retry message",
) {
Column {
Text("Could not refresh the list.")
Button(
onClick = onRetry,
modifier = Modifier.animateEnterExit(
enter = slideInVertically(),
exit = slideOutVertically(),
label = "retry action",
),
) {
Text("Retry")
}
}
}
}If every child needs its own motion, set the parent enter and exit to EnterTransition.None and ExitTransition.None. The parent still manages lifecycle; it simply does not add a second visual effect. Android’s animation composables documentation covers both combined parent/child motion and this opt-out pattern.
Custom exit work must join the visibility transition
Inside AnimatedVisibility, the receiver exposes a transition. Values animated through that transition run with the enter/exit transition and delay removal until they finish:
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.core.animateColor
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun StatusIndicator(visible: Boolean) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut(),
label = "status indicator",
) {
val color by transition.animateColor(label = "status color") { state ->
if (state == EnterExitState.Visible) Color(0xFF0F766E) else Color.Gray
}
androidx.compose.foundation.layout.Box(
Modifier
.size(64.dp)
.background(color),
)
}
}This is a subtle but important boundary. AnimatedVisibility waits for animations added to its transition, but it cannot wait for independent exit animations such as animate*AsState inside the child. The official guide warns that those independent animations can be cut short when the child is removed. For additional values that must finish before removal, use transition.animate* in this scope.
For a single value that remains visible throughout its animation, animate*AsState is still the simpler API. Choose based on lifecycle, not on whether both examples happen to animate alpha.
Start immediately or observe lifecycle with MutableTransitionState
The Boolean overload is right for most screens. Use MutableTransitionState<Boolean> when a newly added AnimatedVisibility should begin hidden and immediately animate in, or when you need to observe whether it is appearing, visible, disappearing, or invisible.
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@Composable
fun AppearingNotice() {
val noticeState = remember {
MutableTransitionState(false).apply {
targetState = true
}
}
AnimatedVisibility(visibleState = noticeState) {
Text("Saved locally")
}
val status = when {
noticeState.isIdle && noticeState.currentState -> "visible"
!noticeState.isIdle && noticeState.currentState -> "disappearing"
noticeState.isIdle && !noticeState.currentState -> "invisible"
else -> "appearing"
}
}currentState is the state that the transition has reached, targetState is where it is heading, and isIdle tells you whether there is pending animation. The API reference for MutableTransitionState documents those properties. Do not use lifecycle observation to put business work after a visual animation unless that timing is truly part of the product behavior.
Removing items from a list without skipping the exit
For a list item, deletion and visibility are related but not identical:
- Keep the item in the displayed collection and set its visibility target to
false. - Let its
AnimatedVisibilityfinish the exit transition. - Remove the data item after the transition reaches an invisible idle state.
If you remove the item from the collection in step 1, the whole composable disappears immediately and there is nothing left to animate. Keep stable list keys while it remains in the layout; lazy list keys and item animations explains why identity matters when list content changes.
For loading, empty, error, and retry content, animate only when it clarifies a state change. The primary need is still a truthful UI state; see loading, empty, error, and retry states for that state model.
Common mistakes
Animating alpha instead of visibility
An alpha animation can be correct for a temporary visual dim. It does not remove the node, reclaim layout space, or automatically take it out of the accessibility tree. Use AnimatedVisibility when the content genuinely goes away.
Driving visibility from transient rendering logic
Keep the Boolean stable and meaningful. If the condition changes on every scroll pixel or recomposition, the UI can reverse direction repeatedly. Derive a threshold first when appropriate; derivedStateOf shows that pattern.
Adding independent exit animation inside the content
Do not rely on animate*AsState for a child animation that must finish before removal. Use the AnimatedVisibilityScope.transition for custom work that belongs to the enter/exit lifecycle.
Using it for content replacement
When old and new content represent different states and should cross or slide through each other, use AnimatedContent. AnimatedVisibility controls whether one content tree exists at all.
Test the state boundary and the transition
Test that the content is present while visible and absent after the exit completes. In a Compose UI test, control the animation clock when you need to assert intermediate or final timing. Also test a rapid true → false → true sequence; a visibility transition should reverse cleanly without deleting the content prematurely.
Give the animation a descriptive label and inspect it in Android Studio’s Animation Preview. It supports AnimatedVisibility alongside other Compose animation APIs.
FAQ
Does AnimatedVisibility remove content immediately when visible becomes false?
No. It starts the exit transition and removes the content after the transition work it owns finishes.
Can I use AnimatedVisibility inside a LazyColumn?
Yes, but retain the item long enough for its exit transition. Removing its data item immediately prevents the exit animation.
Should I use it for a screen navigation transition?
Usually no. Route changes have navigation lifecycle and back-stack concerns. Use the transitions provided by your navigation setup; reserve AnimatedVisibility for content within a screen.