Smooth Transitions Made Easy: AnimatedVisibility, AnimatedContent, and animateContentSize Explained

Glory OlaifaGlory Olaifa
8 min read

When you build a modern Android app, your goal isn’t just to display information — it’s to communicate with the user. Part of that conversation is how your UI changes in response to what the user does.

Does a button press make something appear instantly, snapping into view? Or does it slide in naturally, catching the eye gently without feeling jarring?
These differences might sound subtle, but they’re what separates an app that feels clunky from one that feels polished, modern, and delightful to use.

In the early days of Android, adding animations was tedious. You’d have to create separate XML animation files, tie them together with code, and pray they’d sync well. If you wanted to animate a layout size, you’d often be out of luck or writing fragile custom code.

Then came Jetpack Compose — Android’s modern, declarative UI toolkit. Compose makes animations so much easier because the same code that describes what your UI should look like can also describe how it should change.

In this guide, we’ll explore three incredibly useful Compose APIs that make it effortless to create smooth transitions:

  • AnimatedVisibility: for showing and hiding parts of your UI with a nice entrance or exit.

  • AnimatedContent: for smoothly changing what’s inside a container, like swapping text or views.

  • animateContentSize: for automatically animating size changes when your content grows or shrinks.

To make sure you really get it, I’ll break each example down line by line so you understand not just what works, but why. By the end, you’ll know exactly when to reach for each tool.

2. AnimatedVisibility

Let’s start with AnimatedVisibility. This is your go-to when you have an element, such as card, a button, or a menu, that should appear or disappear smoothly.
No more sudden pop-ins or awkward jumps. With just a few lines of code, you get fade, slide, or scale transitions for free.

2.1 Basic Visibility Toggle

Let’s look at the simplest version first, showing or hiding a Box when a button is tapped.

var visible by remember { mutableStateOf(true) }

Column(
    modifier = Modifier.fillMaxSize(),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = { visible = !visible }) {
        Text("Toggle Box")
    }

    if (visible) {
        Box(
            modifier = Modifier
                .size(200.dp)
                .clip(RoundedCornerShape(8.dp))
                .background(Color.Cyan)
        )
    }
}

Let’s break this down, line by line:

var visible by remember { mutableStateOf(true) }: This line creates a piece of state called visible. The remember function tells Compose: “Hey, remember this value across recompositions.”

When you change visible, Compose automatically re-runs the composable, showing or hiding the box.
Column(...) { ... } : We wrap everything inside a Column so we can stack the Button on top of the Box.
fillMaxSize() makes the column take up the whole screen.

verticalArrangement = Arrangement.Center centers the column’s children vertically.

horizontalAlignment = Alignment.CenterHorizontally centers them horizontally.

Button(onClick = { visible = !visible }) { Text("Toggle Box") } This button flips visible to its opposite whenever you click it. So, if the box is showing, it hides — and vice versa.

if (visible) { Box(...) }: This is just a plain conditional. If visible is true, Compose puts the Box on the screen. If not, the Box doesn’t exist.

Inside the Box, the modifier:

  • .size(200.dp) gives it a fixed width and height.

  • .clip(RoundedCornerShape(8.dp)) rounds the corners slightly.

  • .background(Color.Cyan) fills it with a bright cyan color.

The problem: This works, but it’s abrupt. The box appears or disappears instantly, which can feel harsh.

2.2 Adding AnimatedVisibility

Now let’s make it smooth with AnimatedVisibility.

AnimatedVisibility(visible) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(Color.Cyan)
    )
}

What’s different?

Instead of if (visible), we wrap the Box inside AnimatedVisibility(visible).

Here’s what happens under the hood:

  • When visible is true, Compose animates the Box into view.

  • When visible becomes false, Compose animates the Box out of view.

By default, AnimatedVisibility uses a simple fade in and fade out, you don’t need to specify anything extra.

With this tiny change, you've just upgraded your abrupt toggle into a gentle fade.

2.3 Adding Custom Enter/Exit

A fade is nice — but Compose lets you do more. Want to slide in? Combine multiple effects? Easy.

AnimatedVisibility(
    visible,
    enter = slideInVertically() + fadeIn(),
    exit = slideOutHorizontally() + fadeOut()
) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(Color.Cyan)
    )
}

Let break this down:

enter = slideInVertically() + fadeIn()
This controls how the box comes in.

  • slideInVertically() means it slides into place vertically, from just offscreen above.

  • + fadeIn() means it fades in while sliding.

exit = slideOutHorizontally() + fadeOut()
This controls how it leaves.

  • slideOutHorizontally() makes it slide sideways off the screen.

  • + fadeOut() makes it fade out at the same time.

The + combines multiple effects. The entrance and exit can be completely different.

Now your box doesn’t just appear, it glides in and out in a way that feels intentional and fluid.

2.4 Control the Timing

Want it to move slower? Or bounce? Compose lets you control the animation’s spec.

AnimatedVisibility(
    visible,
    enter = slideInVertically() + fadeIn(animationSpec = tween(durationMillis = 1000)),
    exit = slideOutHorizontally() + fadeOut()
) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(8.dp))
            .background(Color.Cyan)
    )
}

Here, tween means tweening between values over 1000 milliseconds, so the fade lasts 1 second instead of the default spring.

Key part:

  • fadeIn(animationSpec = tween(durationMillis = 1000))
    This changes the fade to take 1 second instead of the default ~300ms.
    tween means it linearly interpolates the alpha (opacity) over that time.

You can get fancy: spring() gives you a bouncy feel, keyframes() lets you define exact steps.

You might use this for showing or hiding an expandable menu, a modal sheet, or a settings panel. AnimatedVisibility works best when you want to reveal whole blocks of UI, not just tiny parts.

3. AnimateContent

AnimatedContent is for when the content itself changes, not just whether it’s visible.

3.1 Basic AnimatedContent

Imagine a counter that jumps from 1 to 2 to 3. Instead of snapping the number instantly, AnimatedContent can animate the switch.

var count by remember { mutableStateOf(0) }

Column(
    modifier = Modifier.fillMaxSize(),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = { count++ }) {
        Text("Increment")
    }

    AnimatedContent(targetState = count) { targetCount ->
        Text(
            text = "$targetCount",
            style = MaterialTheme.typography.displayMedium
        )
    }
}

Tap the button — you’ll see the number crossfade between states.

AnimatedContent(targetState = count) { targetCount -> ... }
This is the magic. Compose watches count. When it changes, the content inside AnimatedContent is replaced, but animated instead of just swapped.
Inside, we show Text("$targetCount").

By default, Compose crossfades the old and new numbers.

3.2 Custom TransitionSpec

A fade is fine, but let’s make the new number slide up while the old one slides away.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        slideInVertically { it } + fadeIn() with
        slideOutVertically { -it } + fadeOut()
    }
) { targetCount ->
    Text(
        text = "$targetCount",
        style = MaterialTheme.typography.displayMedium
    )
}

Here:

  • slideInVertically { it } means slide in from the bottom (it is the height).

  • slideOutVertically { -it } slides out up.

  • + fadeIn() and + fadeOut() add opacity.

  • with ties the entrance and exit together.

Now, when you tap, the old number slides away while the new one slides in — way more dynamic!

3.3 SizeTransform

If your content changes size, AnimatedContent can animate that too.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        fadeIn() with fadeOut() using SizeTransform(clip = false)
    }
) { targetCount ->
    Text(
        text = if (targetCount % 2 == 0) "Even: $targetCount" else "Odd: $targetCount",
        style = MaterialTheme.typography.displayMedium
    )
}

What’s new?

Here, the SizeTransform smoothly resizes the container when the text length changes.

  • The text length changes because we prefix with Even: or Odd:.

  • SizeTransform means Compose smoothly resizes the parent as the text grows or shrinks.

This keeps the switch elegant, avoiding layout jumps.

4. animateContentSize

What if you don’t want to manage enter/exit transitions but still want nice-sized animations? That’s where animateContentSize comes in.

4.1 Basic Example

var expanded by remember { mutableStateOf(false) }

Box(
    modifier = Modifier
        .clickable { expanded = !expanded }
        .background(Color.Green)
        .animateContentSize()
        .size(if (expanded) 200.dp else 100.dp)
)

Tap the box — it smoothly grows and shrinks. No extra work!

What’s happening up there?

  • Tapping toggles expanded.

  • If expanded, the box is 200dp. If not, 100dp.

  • .animateContentSize() watches for any size change and animates it automatically.

4.2 Expandable Text

var expanded by remember { mutableStateOf(false) }

Column(
    modifier = Modifier
        .padding(16.dp)
        .clickable { expanded = !expanded }
        .background(Color.LightGray)
        .animateContentSize()
        .padding(16.dp)
) {
    Text("Click to ${if (expanded) "collapse" else "expand"}")

    if (expanded) {
        Text("Here's more detail that appears smoothly when expanded. animateContentSize makes this easy.")
    }
}

Perfect for FAQs, accordions, or “Read more” sections. No need to define enter/exit animations, animateContentSize detects size change and animates it for you.

4.3 Tweak the Spring

You can customize how snappy the size change feels:

.animateContentSize(
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioMediumBouncy,
        stiffness = Spring.StiffnessLow
    )
)

Higher stiffness = quicker animation. Lower dampingRatio = more bounce.

Use animateContentSize for expanding cards, FAQ sections, or any time you have collapsible content. It works well with LazyColumn too!

Conclusion

Animations shouldn’t be complicated. With AnimatedVisibility, AnimatedContent, and animateContentSize, you get smooth transitions with just a few lines of code.

You don’t need to be an animation guru; Compose’s declarative approach handles the math and timing for you.

So next time you hide a menu, update content, or expand a card, reach for these tools. Your UI — and your users — will thank you.

0
Subscribe to my newsletter

Read articles from Glory Olaifa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Glory Olaifa
Glory Olaifa

Glory Olaifa is an accomplished mobile engineer with a rich background spanning 4 years in the industry. He has demonstrated exceptional leadership skills, successfully guiding a team of 4 engineers in the development of a high-quality, production-ready application. A two-time hackathon winner and 2024 Google Solution winner, Glory is deeply passionate about community building. He recently spearheads the Google Developer Student Club at a prestigious university, where he actively nurtures a culture of learning and collaboration among students. In addition to his role at the university, Glory serves as the lead organizer for Flutter Ogbomosho, a position that highlights his dedication to fostering a vibrant developer community. His commitment to empowering others and his technical expertise make him a valuable asset to any team or project. Outside of his professional achievements, Glory enjoys playing football, video games, and sprinting. He also loves to party occasionally, embracing a well-rounded approach to life.