Mastering Animations in Compose: From Simple to Complex

By Engineering Team • Published: 2025-12-12 • Updated: 2025-12-1216 min read

AnimationMotionUI PolishUser Experience

Animation Philosophy in Compose

Compose animations are state-driven. You change state, and the framework animates between old and new values. This declarative approach eliminates the imperative animation code that was error-prone in Views.

The key insight: you do not tell Compose HOW to animate, you tell it WHAT values to animate between, and it figures out the rest.

animate*AsState: The Simplest Animation

For animating single values, animate*AsState is your go-to. It creates a smooth transition whenever the target value changes:

@Composable
fun ExpandableCard(isExpanded: Boolean, content: @Composable () -> Unit) {
    // Animates smoothly between 100dp and 300dp
    val height by animateDpAsState(
        targetValue = if (isExpanded) 300.dp else 100.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "card height"
    )
    
    Card(modifier = Modifier.height(height)) {
        content()
    }
}

AnimatedVisibility: Enter and Exit

AnimatedVisibility handles appearing and disappearing content with customizable transitions:

@Composable
fun NotificationBanner(message: String?, onDismiss: () -> Unit) {
    AnimatedVisibility(
        visible = message != null,
        enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
        exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
    ) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Row(
                modifier = Modifier.padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(message ?: "")
                IconButton(onClick = onDismiss) {
                    Icon(Icons.Default.Close, contentDescription = "Dismiss")
                }
            }
        }
    }
}

Transition: Coordinating Multiple Animations

When multiple values need to animate together, use updateTransition. It ensures all animations start and end at the same time:

enum class CardState { Collapsed, Expanded }

@Composable
fun CoordinatedCard(state: CardState) {
    val transition = updateTransition(state, label = "card transition")
    
    val height by transition.animateDp(label = "height") { cardState ->
        when (cardState) {
            CardState.Collapsed -> 100.dp
            CardState.Expanded -> 300.dp
        }
    }
    
    val cornerRadius by transition.animateDp(label = "corner") { cardState ->
        when (cardState) {
            CardState.Collapsed -> 16.dp
            CardState.Expanded -> 8.dp
        }
    }
    
    val backgroundColor by transition.animateColor(label = "color") { cardState ->
        when (cardState) {
            CardState.Collapsed -> Color.LightGray
            CardState.Expanded -> Color.White
        }
    }
    
    Card(
        modifier = Modifier.height(height),
        shape = RoundedCornerShape(cornerRadius),
        colors = CardDefaults.cardColors(containerColor = backgroundColor)
    ) {
        // Content
    }
}

Performance Tips

Animations can be performance-intensive. Follow these guidelines:

  • Prefer graphicsLayer modifiers for translation, rotation, scale, and alpha — they avoid recomposition
  • Use derivedStateOf to debounce rapid state changes
  • Provide meaningful labels to all animations for debugging in Layout Inspector
  • Test on low-end devices to catch performance issues early
Note: Always respect user preferences. Check LocalAccessibilityManager for users who have enabled "reduce motion" in system settings.
← Back to Blog