How to Master AnimatedContent in Jetpack Compose: Build Smooth UI Transitions

Table of Contents

Good UI doesn’t just look nice, it moves well. Small, thoughtful animations help users understand what changed and why. In Jetpack Compose, AnimatedContent makes this surprisingly easy.

This guide walks you through how it works, when to use it, and how to keep things clean and performant.

What is AnimatedContent?

AnimatedContent is a composable in Jetpack Compose that automatically animates between different UI states.

Instead of abruptly switching content, it smoothly transitions from one state to another.

Think of it like this:

  • Without AnimatedContent → content just changes
  • With AnimatedContent → content transforms into the next state

When Should You Use AnimatedContent?

Use AnimatedContent when:

  • You switch between UI states (loading → success → error)
  • You update text, numbers, or layouts dynamically
  • You want smooth transitions without managing animations manually

Basic Example of AnimatedContent

Kotlin
@Composable
fun SimpleAnimatedContentExample(count: Int) {
    AnimatedContent(targetState = count) { targetCount ->
        Text(
            text = "Count: $targetCount",
            fontSize = 24.sp
        )
    }
}
  • targetState = count → tells Compose what state to watch
  • When count changes → animation is triggered
  • targetCount → the new value inside the animation block

This is the core idea of AnimatedContent.

Let’s build a simple counter with animation.

Kotlin
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    Column(
        horizontalAlignment =
            Alignment.CenterHorizontally
    ) {
        AnimatedContent(targetState = count) { value ->
            Text(
                text = "$value",
                fontSize = 40.sp
            )
        }
        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = { count++ }) {
            Text("Increase")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
    CenteredPreview {
        CounterScreen()
    }
}

Here,

Every time count changes:

  • Old text fades/slides out
  • New text animates in

No extra animation code needed.

Customizing Animation in AnimatedContent

By default, animations are nice but basic. You can customize them using transitionSpec.

Example with Slide + Fade

Kotlin
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(12.dp),
        modifier = Modifier.padding(16.dp)
    ) {

        // Increment
        CounterButton(
            text = "+",
            onClick = { count++ }
        )

        // Animated Counter
        RollingCounter(count = count)

        // Decrement
        CounterButton(
            text = "-",
            onClick = { if (count > 0) count-- },
            enabled = count > 0
        )
    }
}

@Composable
fun CounterButton(
    text: String,
    onClick: () -> Unit,
    enabled: Boolean = true
) {
    Button(
        onClick = onClick,
        enabled = enabled,
        shape = CircleShape,
        modifier = Modifier.size(48.dp),
        contentPadding = PaddingValues(0.dp)
    ) {
        Text(
            text = text,
            fontSize = 22.sp
        )
    }
}

@Composable
fun RollingCounter(
    count: Int,
    fontSize: TextUnit = 40.sp
) {
    val digits = count.toString().map { it.toString() }

    Row {
        digits.forEachIndexed { index, digit ->
            key(index) {
                RollingDigit(
                    digit = digit,
                    fontSize = fontSize
                )
            }
        }
    }
}

@Composable
fun RollingDigit(
    digit: String,
    fontSize: TextUnit
) {
    Box(
        modifier = Modifier
            .height(48.dp)
            .width(28.dp),
        contentAlignment = Alignment.Center
    ) {
        AnimatedContent(
            targetState = digit,
            transitionSpec = {
                if (targetState > initialState) {
                    // Increment → roll up
                    slideInVertically { it } + fadeIn() togetherWith
                            slideOutVertically { -it } + fadeOut()
                } else {
                    // Decrement → roll down
                    slideInVertically { -it } + fadeIn() togetherWith
                            slideOutVertically { it } + fadeOut()
                }
            }
        ) { targetDigit ->
            Text(
                text = targetDigit,
                fontSize = fontSize,
                fontFamily = FontFamily.Monospace
            )
        }
    }
}

@Composable
fun CenteredPreview(content: @Composable () -> Unit) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        content()
    }
}

@Preview(showBackground = true)
@Composable
fun CounterPreview() {
    CenteredPreview {
        CounterScreen()
    }
}
  • Digit-by-digit rolling animation
  • Direction-aware transitions (up/down)
  • Creates a smooth vertical transition

Understanding transitionSpec (Simple Explanation)

Inside transitionSpec, you define:

  • Enter animation → how new content appears
  • Exit animation → how old content disappears

You combine them using:

Kotlin
enterAnimation togetherWith exitAnimation

Add Direction-Based Animation

You can change animation depending on state.

Kotlin
AnimatedContent(
    targetState = count,
    transitionSpec = {
        if (targetState > initialState) {
            slideInHorizontally { it } togetherWith
            slideOutHorizontally { -it }
        } else {
            slideInHorizontally { -it } togetherWith
            slideOutHorizontally { it }
        }
    }
) { value ->
    Text(text = "$value", fontSize = 40.sp)
}

What’s happening?

  • Increasing number → slides from right
  • Decreasing number → slides from left

This small detail makes your UI feel intelligent.

Example 2: Switch Between Screens

You can use AnimatedContent to switch UI states like this:

Kotlin
enum class ScreenState {
    LOADING, SUCCESS, ERROR
}

@Composable
fun ScreenExample(state: ScreenState) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp),
        contentAlignment = Alignment.Center
    ) {
        AnimatedContent(
            targetState = state,
            label = "screen_state",
            transitionSpec = {
                when {
                    // Loading → Success
                    initialState == ScreenState.LOADING &&
                            targetState == ScreenState.SUCCESS -> {
                        slideInVertically { it } + fadeIn() togetherWith
                                slideOutVertically { -it } + fadeOut()
                    }

                    // Loading → Error
                    initialState == ScreenState.LOADING &&
                            targetState == ScreenState.ERROR -> {
                        slideInVertically { -it } + fadeIn() togetherWith
                                slideOutVertically { it } + fadeOut()
                    }

                    // Error → Loading (retry)
                    initialState == ScreenState.ERROR &&
                            targetState == ScreenState.LOADING -> {
                        fadeIn() togetherWith fadeOut()
                    }

                    // Default
                    else -> {
                        fadeIn() togetherWith fadeOut()
                    }
                }
            }
        ) { targetState ->
            when (targetState) {

                ScreenState.LOADING -> {
                    CircularProgressIndicator()
                }

                ScreenState.SUCCESS -> {
                    Text(
                        text = "Data Loaded!",
                        fontSize = 20.sp
                    )
                }

                ScreenState.ERROR -> {
                    Column(
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            text = "Something went wrong",
                            fontSize = 20.sp
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        Button(onClick = { /* retry action */ }) {
                            Text("Retry")
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun ScreenContainer() {
    var state by remember { mutableStateOf(ScreenState.LOADING) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = Modifier.padding(16.dp)
    ) {

        ScreenExample(state = state)

        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = { state = ScreenState.LOADING }) {
                Text("Loading")
            }
            Button(onClick = { state = ScreenState.SUCCESS }) {
                Text("Success")
            }
            Button(onClick = { state = ScreenState.ERROR }) {
                Text("Error")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun ScreenPreview() {
    CenteredPreview {
        ScreenContainer()
    }
}

Why this is powerful

Instead of hard switching screens:

  • Each state transition feels natural
  • Improves UX instantly

Common Mistakes to Avoid

1. Forgetting stable state

If your targetState changes too frequently or unpredictably, animations may feel glitchy.

Tip: Use proper state management (remember, ViewModel).

2. Overusing animations

Too many animations can overwhelm users.

Keep it simple:

  • Use animation where it adds clarity
  • Avoid unnecessary motion

3. Heavy UI inside AnimatedContent

If your composable is too complex, animation may lag.

Solution:

  • Keep UI lightweight
  • Break into smaller composables

Performance Tips for AnimatedContent

  • Prefer simple transitions (fade + slide)
  • Avoid recomposing large layouts
  • Use remember wisely
  • Test on low-end devices

Real-World Use Cases

You can use AnimatedContent for:

  • Cart updates in e-commerce apps
  • Switching tabs or filters
  • Form step transitions
  • Notifications or alerts
  • Dashboard value updates

Why AnimatedContent Improves UX

Good animation:

  • Guides attention
  • Explains change
  • Reduces confusion

AnimatedContent does this automatically, which saves time and improves quality.

Conclusion

Mastering AnimatedContent is less about memorizing APIs and more about understanding when and why to animate.

Start small:

  • Animate text
  • Animate numbers
  • Then move to full UI transitions

Over time, you’ll naturally build smoother, more polished apps.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!